├── .github ├── FUNDING.yml ├── linters │ └── .markdown-lint.yml └── workflows │ ├── add-discuss-during-sync.yml │ ├── announce-a-release.yml │ ├── breakage-against-linux-ponyc-latest.yml │ ├── breakage-against-windows-ponyc-latest.yml │ ├── changelog-bot.yml │ ├── generate-documentation.yml │ ├── lint-action-workflows.yml │ ├── pr.yml │ ├── prepare-for-a-release.yml │ ├── release-notes-reminder.yml │ ├── release-notes.yml │ ├── release.yml │ └── remove-discuss-during-sync.yml ├── .markdownlintignore ├── .release-notes ├── 0.2.7.md ├── 0.2.8.md ├── 0.3.0.md ├── 0.3.1.md ├── 0.4.0.md ├── 0.4.1.md ├── 0.4.2.md ├── 0.4.3.md ├── 0.4.4.md ├── 0.4.5.md ├── 0.5.0.md ├── 0.5.1.md ├── 0.5.2.md ├── 0.5.3.md ├── 0.5.4.md ├── 0.5.5.md ├── 0.6.0.md ├── 0.6.1.md ├── 0.6.2.md └── next-release.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── RELEASE_PROCESS.md ├── STYLE_GUIDE.md ├── VERSION ├── bench └── bench.pony ├── corral.json ├── examples └── httpget │ ├── cacert.pem │ └── httpget.pony ├── http ├── _client_conn_handler.pony ├── _client_connection.pony ├── _host_service.pony ├── _test.pony ├── _test_client.pony ├── _test_client_error_handling.pony ├── http.pony ├── http_client.pony ├── http_handler.pony ├── http_parser.pony ├── http_session.pony ├── mimetypes.pony ├── payload.pony ├── status.pony ├── test │ ├── cert.pem │ └── key.pem ├── url.pony └── url_encode.pony └── make.ps1 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: ponyc 2 | -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/add-discuss-during-sync.yml: -------------------------------------------------------------------------------- 1 | name: Add discuss during sync label 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | issue_comment: 9 | types: 10 | - created 11 | pull_request_target: 12 | types: 13 | - opened 14 | - edited 15 | - ready_for_review 16 | - reopened 17 | pull_request_review: 18 | types: 19 | - submitted 20 | 21 | permissions: 22 | pull-requests: write 23 | 24 | jobs: 25 | add-label: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Add "discuss during sync" label to active GH entity 29 | uses: andymckay/labeler@467347716a3bdbca7f277cb6cd5fa9c5205c5412 30 | with: 31 | repo-token: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 32 | add-labels: "discuss during sync" 33 | -------------------------------------------------------------------------------- /.github/workflows/announce-a-release.yml: -------------------------------------------------------------------------------- 1 | name: Announce a release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'announce-[0-9]+.[0-9]+.[0-9]+' 7 | 8 | concurrency: announce-a-release 9 | 10 | permissions: 11 | packages: read 12 | contents: write 13 | 14 | jobs: 15 | announce: 16 | name: Announcements 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout main 20 | uses: actions/checkout@v4.1.1 21 | with: 22 | ref: "main" 23 | token: ${{ secrets.RELEASE_TOKEN }} 24 | - name: Release notes 25 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 26 | with: 27 | entrypoint: publish-release-notes-to-github 28 | env: 29 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 30 | - name: Zulip 31 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 32 | with: 33 | entrypoint: send-announcement-to-pony-zulip 34 | env: 35 | ZULIP_API_KEY: ${{ secrets.ZULIP_RELEASE_API_KEY }} 36 | ZULIP_EMAIL: ${{ secrets.ZULIP_RELEASE_EMAIL }} 37 | - name: Last Week in Pony 38 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 39 | with: 40 | entrypoint: add-announcement-to-last-week-in-pony 41 | env: 42 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 43 | 44 | post-announcement: 45 | name: Tasks to run after the release has been announced 46 | needs: 47 | - announce 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout main 51 | uses: actions/checkout@v4.1.1 52 | with: 53 | ref: "main" 54 | token: ${{ secrets.RELEASE_TOKEN }} 55 | - name: Rotate release notes 56 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 57 | with: 58 | entrypoint: rotate-release-notes 59 | env: 60 | GIT_USER_NAME: "Ponylang Main Bot" 61 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 62 | - name: Delete announcement trigger tag 63 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 64 | with: 65 | entrypoint: delete-announcement-tag 66 | env: 67 | GIT_USER_NAME: "Ponylang Main Bot" 68 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 69 | -------------------------------------------------------------------------------- /.github/workflows/breakage-against-linux-ponyc-latest.yml: -------------------------------------------------------------------------------- 1 | name: Linux ponyc update breakage test 2 | 3 | on: 4 | repository_dispatch: 5 | types: [shared-docker-linux-builders-updated] 6 | 7 | permissions: 8 | packages: read 9 | 10 | jobs: 11 | libressl-3-vs-ponyc-latest: 12 | name: LibreSSL 3.x with most recent ponyc latest 13 | runs-on: ubuntu-latest 14 | container: 15 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-libressl-3.9.2:latest 16 | steps: 17 | - uses: actions/checkout@v4.1.1 18 | - name: Test 19 | run: make test ssl=0.9.0 config=debug 20 | - name: Send alert on failure 21 | if: ${{ failure() }} 22 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 23 | with: 24 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 25 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 26 | organization-url: 'https://ponylang.zulipchat.com/' 27 | to: notifications 28 | type: stream 29 | topic: ${{ github.repository }} scheduled job failure 30 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 31 | 32 | 33 | libressl-4-vs-ponyc-latest: 34 | name: LibreSSL 4.x with most recent ponyc latest 35 | runs-on: ubuntu-latest 36 | container: 37 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-libressl-4.0.0:latest 38 | steps: 39 | - uses: actions/checkout@v4.1.1 40 | - name: Test 41 | run: make test ssl=0.9.0 config=debug 42 | - name: Send alert on failure 43 | if: ${{ failure() }} 44 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 45 | with: 46 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 47 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 48 | organization-url: 'https://ponylang.zulipchat.com/' 49 | to: notifications 50 | type: stream 51 | topic: ${{ github.repository }} scheduled job failure 52 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 53 | 54 | openssl-1-vs-ponyc-release: 55 | name: OpenSSL 1.x with most recent ponyc latest 56 | runs-on: ubuntu-latest 57 | container: 58 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-openssl_1.1.1w:latest 59 | steps: 60 | - uses: actions/checkout@v4.1.1 61 | - name: Test 62 | run: make test config=debug ssl=1.1.x 63 | - name: Send alert on failure 64 | if: ${{ failure() }} 65 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 66 | with: 67 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 68 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 69 | organization-url: 'https://ponylang.zulipchat.com/' 70 | to: notifications 71 | type: stream 72 | topic: ${{ github.repository }} scheduled job failure 73 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 74 | 75 | openssl-3-vs-ponyc-release: 76 | name: OpenSSL 3.x with most recent ponyc latest 77 | runs-on: ubuntu-latest 78 | container: 79 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-openssl_3.4.1:latest 80 | steps: 81 | - uses: actions/checkout@v4.1.1 82 | - name: Test 83 | run: make test config=debug ssl=3.0.x 84 | - name: Send alert on failure 85 | if: ${{ failure() }} 86 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 87 | with: 88 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 89 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 90 | organization-url: 'https://ponylang.zulipchat.com/' 91 | to: notifications 92 | type: stream 93 | topic: ${{ github.repository }} scheduled job failure 94 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 95 | -------------------------------------------------------------------------------- /.github/workflows/breakage-against-windows-ponyc-latest.yml: -------------------------------------------------------------------------------- 1 | name: Windows ponyc update breakage test 2 | 3 | on: 4 | repository_dispatch: 5 | types: [ponyc-windows-nightly-released] 6 | 7 | permissions: 8 | packages: read 9 | 10 | jobs: 11 | windows-vs-ponyc-latest: 12 | name: Test against recent ponyc release on Windows 13 | runs-on: windows-2022 14 | steps: 15 | - uses: actions/checkout@v4.1.1 16 | - name: Test against recent ponyc release on Windows 17 | run: | 18 | Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/nightlies/raw/versions/latest/ponyc-x86-64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; 19 | Expand-Archive -Force -Path C:\ponyc.zip -DestinationPath C:\ponyc; 20 | Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/corral-x86-64-pc-windows-msvc.zip -OutFile C:\corral.zip; 21 | Expand-Archive -Force -Path C:\corral.zip -DestinationPath C:\ponyc; 22 | $env:PATH = 'C:\ponyc\bin;' + $env:PATH; 23 | .\make.ps1 -Command test 2>&1; 24 | - name: Send alert on failure 25 | if: ${{ failure() }} 26 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 27 | with: 28 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 29 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 30 | organization-url: 'https://ponylang.zulipchat.com/' 31 | to: notifications 32 | type: stream 33 | topic: ${{ github.repository }} scheduled job failure 34 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 35 | -------------------------------------------------------------------------------- /.github/workflows/changelog-bot.yml: -------------------------------------------------------------------------------- 1 | name: Changelog Bot 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - CHANGELOG.md 11 | 12 | permissions: 13 | packages: read 14 | pull-requests: read 15 | contents: write 16 | 17 | jobs: 18 | changelog-bot: 19 | runs-on: ubuntu-latest 20 | name: Update CHANGELOG.md 21 | steps: 22 | - name: Update Changelog 23 | uses: docker://ghcr.io/ponylang/changelog-bot-action:0.3.7 24 | with: 25 | GIT_USER_NAME: "Ponylang Main Bot" 26 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Send alert on failure 30 | if: ${{ failure() }} 31 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 32 | with: 33 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 34 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 35 | organization-url: 'https://ponylang.zulipchat.com/' 36 | to: notifications 37 | type: stream 38 | topic: ${{ github.repository }} unattended job failure 39 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 40 | -------------------------------------------------------------------------------- /.github/workflows/generate-documentation.yml: -------------------------------------------------------------------------------- 1 | name: Manually generate documentation 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | packages: read 11 | 12 | concurrency: 13 | group: "update-documentation" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | generate-documentation: 18 | name: Generate documentation for release 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | container: 24 | image: ghcr.io/ponylang/library-documentation-action-v2-insiders:release 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4.1.1 28 | - name: Generate documentation 29 | run: /entrypoint.py 30 | env: 31 | INPUT_SITE_URL: "https://ponylang.github.io/http/" 32 | INPUT_LIBRARY_NAME: "http" 33 | INPUT_DOCS_BUILD_DIR: "build/http-docs" 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: 'build/http-docs/site/' 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v4 43 | -------------------------------------------------------------------------------- /.github/workflows/lint-action-workflows.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Action Workflows 2 | 3 | on: pull_request 4 | 5 | concurrency: 6 | group: lint-actions-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | permissions: 10 | packages: read 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4.1.1 19 | - name: Check workflow files 20 | uses: docker://ghcr.io/ponylang/shared-docker-ci-actionlint:20250119 21 | with: 22 | args: -color 23 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: pull_request 4 | 5 | concurrency: 6 | group: pr-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | permissions: 10 | packages: read 11 | 12 | jobs: 13 | superlinter: 14 | name: Lint bash, docker, markdown, and yaml 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4.1.1 18 | - name: Lint codebase 19 | uses: docker://github/super-linter:v3.8.3 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | VALIDATE_ALL_CODEBASE: true 23 | VALIDATE_BASH: true 24 | VALIDATE_DOCKERFILE: true 25 | VALIDATE_MD: true 26 | VALIDATE_YAML: true 27 | 28 | verify-changelog: 29 | name: Verify CHANGELOG is valid 30 | runs-on: ubuntu-latest 31 | container: 32 | image: ghcr.io/ponylang/changelog-tool:release 33 | steps: 34 | - uses: actions/checkout@v4.1.1 35 | - name: Verify CHANGELOG 36 | run: changelog-tool verify 37 | 38 | libressl-3-vs-ponyc-release: 39 | name: LibreSSL 3.x with most recent ponyc release 40 | runs-on: ubuntu-latest 41 | container: 42 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-libressl-3.9.2:release 43 | steps: 44 | - uses: actions/checkout@v4.1.1 45 | - name: Test 46 | run: make test config=debug ssl=0.9.0 47 | 48 | libressl-4-vs-ponyc-release: 49 | name: LibreSSL 4.x with most recent ponyc release 50 | runs-on: ubuntu-latest 51 | container: 52 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-libressl-4.0.0:release 53 | steps: 54 | - uses: actions/checkout@v4.1.1 55 | - name: Test 56 | run: make test config=debug ssl=0.9.0 57 | 58 | openssl-1-vs-ponyc-release: 59 | name: OpenSSL 1.x with most recent ponyc release 60 | runs-on: ubuntu-latest 61 | container: 62 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-openssl_1.1.1w:release 63 | steps: 64 | - uses: actions/checkout@v4.1.1 65 | - name: Test 66 | run: make test config=debug ssl=1.1.x 67 | 68 | openssl-3-vs-ponyc-release: 69 | name: OpenSSL 3.x with most recent ponyc release 70 | runs-on: ubuntu-latest 71 | container: 72 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-openssl_3.4.1:release 73 | steps: 74 | - uses: actions/checkout@v4.1.1 75 | - name: Test 76 | run: make test config=debug ssl=3.0.x 77 | 78 | windows-vs-ponyc-release: 79 | name: Windows with most recent ponyc release 80 | runs-on: windows-2022 81 | steps: 82 | - uses: actions/checkout@v4.1.1 83 | - name: Test against recent ponyc release on Windows 84 | run: | 85 | Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/ponyc-x86-64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; 86 | Expand-Archive -Force -Path C:\ponyc.zip -DestinationPath C:\ponyc; 87 | Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/corral-x86-64-pc-windows-msvc.zip -OutFile C:\corral.zip; 88 | Expand-Archive -Force -Path C:\corral.zip -DestinationPath C:\ponyc; 89 | $env:PATH = 'C:\ponyc\bin;' + $env:PATH; 90 | .\make.ps1 -Command test 2>&1; 91 | -------------------------------------------------------------------------------- /.github/workflows/prepare-for-a-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare for a release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'release-[0-9]+.[0-9]+.[0-9]+' 7 | 8 | concurrency: prepare-for-a-release 9 | 10 | permissions: 11 | packages: read 12 | contents: write 13 | 14 | jobs: 15 | # all tasks that need to be done before we add an X.Y.Z tag 16 | # should be done as a step in the pre-tagging job. 17 | # think of it like this... if when you later checkout the tag for a release, 18 | # should the change be there? if yes, do it here. 19 | pre-tagging: 20 | name: Tasks run before tagging the release 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout main 24 | uses: actions/checkout@v4.1.1 25 | with: 26 | ref: "main" 27 | token: ${{ secrets.RELEASE_TOKEN }} 28 | - name: Update CHANGELOG.md 29 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 30 | with: 31 | entrypoint: update-changelog-for-release 32 | env: 33 | GIT_USER_NAME: "Ponylang Main Bot" 34 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 35 | - name: Update VERSION 36 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 37 | with: 38 | entrypoint: update-version-in-VERSION 39 | env: 40 | GIT_USER_NAME: "Ponylang Main Bot" 41 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 42 | - name: Update version in README 43 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 44 | with: 45 | entrypoint: update-version-in-README 46 | env: 47 | GIT_USER_NAME: "Ponylang Main Bot" 48 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 49 | - name: Update corral.json 50 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 51 | with: 52 | entrypoint: update-version-in-corral-json 53 | env: 54 | GIT_USER_NAME: "Ponylang Main Bot" 55 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 56 | 57 | # tag for release 58 | # this will kick off the next stage of the release process 59 | # no additional steps should be added to this job 60 | tag-release: 61 | name: Tag the release 62 | needs: 63 | - pre-tagging 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Checkout main 67 | uses: actions/checkout@v4.1.1 68 | with: 69 | ref: "main" 70 | token: ${{ secrets.RELEASE_TOKEN }} 71 | - name: Trigger artefact creation 72 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 73 | with: 74 | entrypoint: trigger-artefact-creation 75 | env: 76 | GIT_USER_NAME: "Ponylang Main Bot" 77 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 78 | 79 | # all cleanup tags that should happen after tagging for release should happen 80 | # in the post-tagging job. examples of things you might do: 81 | # add a new unreleased section to a changelog 82 | # set a version back to "snapshot" 83 | # in general, post-tagging is for "going back to normal" from tasks that were 84 | # done during the pre-tagging job 85 | post-tagging: 86 | name: Tasks run after tagging the release 87 | needs: 88 | - tag-release 89 | runs-on: ubuntu-latest 90 | steps: 91 | - name: Checkout main 92 | uses: actions/checkout@v4.1.1 93 | with: 94 | ref: "main" 95 | token: ${{ secrets.RELEASE_TOKEN }} 96 | - name: Add "unreleased" section to CHANGELOG.md 97 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 98 | with: 99 | entrypoint: add-unreleased-section-to-changelog 100 | env: 101 | GIT_USER_NAME: "Ponylang Main Bot" 102 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 103 | -------------------------------------------------------------------------------- /.github/workflows/release-notes-reminder.yml: -------------------------------------------------------------------------------- 1 | name: Release Notes Reminder 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - labeled 7 | 8 | permissions: 9 | packages: read 10 | pull-requests: write 11 | 12 | jobs: 13 | release-note-reminder: 14 | runs-on: ubuntu-latest 15 | name: Prompt to add release notes 16 | steps: 17 | - name: Prompt to add release notes 18 | uses: docker://ghcr.io/ponylang/release-notes-reminder-bot-action:0.1.1 19 | env: 20 | API_CREDENTIALS: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/release-notes.yml: -------------------------------------------------------------------------------- 1 | name: Release Notes 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - .release-notes/next-release.md 11 | - .release-notes/\d+.\d+.\d+.md 12 | 13 | permissions: 14 | packages: read 15 | pull-requests: read 16 | contents: write 17 | 18 | jobs: 19 | release-notes: 20 | runs-on: ubuntu-latest 21 | name: Update release notes 22 | steps: 23 | - name: Update 24 | uses: docker://ghcr.io/ponylang/release-notes-bot-action:0.3.10 25 | with: 26 | GIT_USER_NAME: "Ponylang Main Bot" 27 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 28 | env: 29 | API_CREDENTIALS: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Send alert on failure 31 | if: ${{ failure() }} 32 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 33 | with: 34 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 35 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 36 | organization-url: 'https://ponylang.zulipchat.com/' 37 | to: notifications 38 | type: stream 39 | topic: ${{ github.repository }} scheduled job failure 40 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | concurrency: release 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | packages: read 15 | 16 | jobs: 17 | # validation to assure that we should in fact continue with the release should 18 | # be done here. the primary reason for this step is to verify that the release 19 | # was started correctly by pushing a `release-X.Y.Z` tag rather than `X.Y.Z`. 20 | pre-artefact-creation: 21 | name: Tasks to run before artefact creation 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout main 25 | uses: actions/checkout@v4.1.1 26 | with: 27 | ref: "main" 28 | token: ${{ secrets.RELEASE_TOKEN }} 29 | - name: Validate CHANGELOG 30 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 31 | with: 32 | entrypoint: pre-artefact-changelog-check 33 | 34 | generate-documentation: 35 | name: Generate documentation for release 36 | environment: 37 | name: github-pages 38 | url: ${{ steps.deployment.outputs.page_url }} 39 | runs-on: ubuntu-latest 40 | needs: 41 | - pre-artefact-creation 42 | container: 43 | image: ghcr.io/ponylang/library-documentation-action-v2-insiders:release 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4.1.1 47 | with: 48 | ref: "main" 49 | token: ${{ secrets.RELEASE_TOKEN }} 50 | - name: Generate documentation 51 | run: /entrypoint.py 52 | env: 53 | INPUT_SITE_URL: "https://ponylang.github.io/http/" 54 | INPUT_LIBRARY_NAME: "http" 55 | INPUT_DOCS_BUILD_DIR: "build/http-docs" 56 | - name: Setup Pages 57 | uses: actions/configure-pages@v5 58 | - name: Upload artifact 59 | uses: actions/upload-pages-artifact@v3 60 | with: 61 | path: 'build/http-docs/site/' 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | 66 | trigger-release-announcement: 67 | name: Trigger release announcement 68 | runs-on: ubuntu-latest 69 | needs: 70 | - generate-documentation 71 | steps: 72 | - uses: actions/checkout@v4.1.1 73 | with: 74 | ref: "main" 75 | token: ${{ secrets.RELEASE_TOKEN }} 76 | - name: Trigger 77 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 78 | with: 79 | entrypoint: trigger-release-announcement 80 | env: 81 | GIT_USER_NAME: "Ponylang Main Bot" 82 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 83 | -------------------------------------------------------------------------------- /.github/workflows/remove-discuss-during-sync.yml: -------------------------------------------------------------------------------- 1 | name: Remove discuss during sync label 2 | 3 | on: 4 | issues: 5 | types: 6 | - closed 7 | pull_request_target: 8 | types: 9 | - closed 10 | 11 | permissions: 12 | pull-requests: write 13 | 14 | jobs: 15 | remove-label: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Remove label 19 | uses: andymckay/labeler@467347716a3bdbca7f277cb6cd5fa9c5205c5412 20 | with: 21 | repo-token: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 22 | remove-labels: "discuss during sync" 23 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | CODE_OF_CONDUCT.md 3 | .release-notes/ 4 | -------------------------------------------------------------------------------- /.release-notes/0.2.7.md: -------------------------------------------------------------------------------- 1 | ## Internal only release 2 | 3 | This was release was purely for non-public facing changes. 4 | -------------------------------------------------------------------------------- /.release-notes/0.2.8.md: -------------------------------------------------------------------------------- 1 | ## Use buffered write for Payload - improves perf by ~200x 2 | 3 | Serialize the `Payload` using a `Writer` and then send a single message 4 | to the `TCPConnection`. 5 | 6 | This improves performance of `HTTPClient` by factor 200 on my system. Before 7 | this change, I got 17 req/s. After the change it's 3333 req/s. 8 | 9 | -------------------------------------------------------------------------------- /.release-notes/0.3.0.md: -------------------------------------------------------------------------------- 1 | ## Remove HTTP server code from repository 2 | 3 | It is obsolete and defect by now. For HTTP servers [ponylang/http_server][1] 4 | should be used. 5 | 6 | [1]: https://github.com/ponylang/http_server 7 | 8 | ## Dont export test related classes 9 | 10 | Prior to this change, internal test related classes were being exported when `use "http"` was done. 11 | 12 | -------------------------------------------------------------------------------- /.release-notes/0.3.1.md: -------------------------------------------------------------------------------- 1 | ## Update net_ssl dependency 2 | 3 | Updated the net_ssl dependency from 1.1.4 to 1.1.5. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.4.0.md: -------------------------------------------------------------------------------- 1 | ## Removed unneeded regex dependency 2 | 3 | Removed no longer needed regex dependency 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.4.1.md: -------------------------------------------------------------------------------- 1 | ## Forward prepare for coming breaking change in ponyc 2 | 3 | This change has no impact on end users, but will future proof against a coming breaking change in the standard library. Users of this version of the library won't be impacted by the coming change. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.4.2.md: -------------------------------------------------------------------------------- 1 | ## Make Payload headers case insensitive 2 | 3 | The HTTP spec says that headers are case-insensitve. That is, "Accept", "ACCEPT", "accept" etc are all the same thing. However, the http library was treating them as different headers. 4 | 5 | All headers are now converted to lowercase for storage in a Payload and all lookups are done using the key converted to lowercase. 6 | 7 | -------------------------------------------------------------------------------- /.release-notes/0.4.3.md: -------------------------------------------------------------------------------- 1 | ## Add Windows support 2 | 3 | Added support for building and testing on Windows. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.4.4.md: -------------------------------------------------------------------------------- 1 | ## Updates to work with ponyc 0.44.0 2 | 3 | [RFC 70](https://github.com/ponylang/rfcs/blob/main/text/0070-filepath-constructor.md) resulted in breaking changes in ponyc. We've updated http so that it works with ponyc 0.44.0. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.4.5.md: -------------------------------------------------------------------------------- 1 | ## Update to work with latest ponyc 2 | 3 | The most recent ponyc implements [RFC #65](https://github.com/ponylang/rfcs/blob/main/text/0065-env-root-not-optional.md) which changes the type of `Env.root`. 4 | 5 | We've updated accordingly. You won't be able to use this and future versions of the library without a corresponding update to your ponyc version. 6 | 7 | -------------------------------------------------------------------------------- /.release-notes/0.5.0.md: -------------------------------------------------------------------------------- 1 | ## Update interfaces with private methods work with Pony 0.47.0 2 | 3 | Interfaces can no longer have private methods. We've updated `HTTPSession` to match. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.5.1.md: -------------------------------------------------------------------------------- 1 | ## Support Ponyup on Windows 2 | 3 | Added changes to the Windows `make.ps1` script to make sure it still works with ponyup on Windows. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.5.2.md: -------------------------------------------------------------------------------- 1 | ## Update to work with Pony 0.49.0 2 | 3 | [Pony 0.49.0](https://github.com/ponylang/ponyc/releases/tag/0.49.0) introduced a lot of different breaking changes. We've updated to account for them all. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.5.3.md: -------------------------------------------------------------------------------- 1 | ## Add support for OpenSSL 3 2 | 3 | We've added support for working with OpenSSL 3 but updating to using `ponylang/net_ssl` 1.3.0 for SSL support. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.5.4.md: -------------------------------------------------------------------------------- 1 | ## Update ponylang/net_ssl dependency 2 | 3 | We've updated our ponylang/net_ssl dependency from version 1.3.0 to version 1.3.1. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.5.5.md: -------------------------------------------------------------------------------- 1 | ## Fix bug triggered by OpenSSL 3.2 2 | 3 | Making HTTPClient calls over SSL when using OpenSSL 3.2.0 would encounter a nasty bug. When executed in a program compiled in release mode, the program would hang. When executed in a program compiled in debug mode, the program would segfault due to infinite recursion. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.6.0.md: -------------------------------------------------------------------------------- 1 | ## Change HTTPClient handler factory creation 2 | 3 | The handler factory for creating handlers for new requests is now provided in the constructor of the client instead of the apply method. This makes it more clear, that the client will use the same handler for all requests. 4 | 5 | The old version would look similar to this: 6 | 7 | ```pony 8 | let client = HTTPClient(auth) 9 | 10 | // Later 11 | let handler_factory = ... 12 | 13 | client(payload, handler_factory)? 14 | 15 | // Even later 16 | client(other_payload, other_factory) 17 | ``` 18 | 19 | In the new version the handler factory needs to be supplied at the creation of the client: 20 | 21 | ```pony 22 | let handler_factory = ... 23 | let client = HTTPClient(auth, handler_factory) 24 | 25 | client(payload) 26 | 27 | // This will use the handler_factory 28 | client(other_payload) 29 | 30 | // To use a different handler factory, create a new client 31 | 32 | let other_client = Client(auth, other_factory) 33 | other_client(other_payload) 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /.release-notes/0.6.1.md: -------------------------------------------------------------------------------- 1 | ## Update LibreSSL version used on Windows 2 | 3 | The version of LibreSSL used on Windows has been updated to 3.9.1. 4 | -------------------------------------------------------------------------------- /.release-notes/0.6.2.md: -------------------------------------------------------------------------------- 1 | ## Fix compilation error with latest pony compiler 2 | 3 | When a soundness hole was fixed in the pony compiler, we started getting a compilation error in this library. We've updated the library to work with the latest pony compiler. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/next-release.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponylang/http/a8d95a14de6f5bb7ff775891963532d2e4e05222/.release-notes/next-release.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a CHANGELOG](http://keepachangelog.com/). 4 | 5 | ## [unreleased] - unreleased 6 | 7 | ### Fixed 8 | 9 | 10 | ### Added 11 | 12 | 13 | ### Changed 14 | 15 | 16 | ## [0.6.2] - 2025-01-26 17 | 18 | ### Fixed 19 | 20 | - Fix compilation error with latest pony compiler ([PR #113](https://github.com/ponylang/http/pull/113)) 21 | 22 | ## [0.6.1] - 2024-04-20 23 | 24 | ### Changed 25 | 26 | - Update LibreSSL version used on Windows ([PR #111](https://github.com/ponylang/http/pull/111)) 27 | 28 | ## [0.6.0] - 2024-01-21 29 | 30 | ### Changed 31 | 32 | - Change HTTPClient handler factory creation ([PR #106](https://github.com/ponylang/http/pull/106)) 33 | 34 | ## [0.5.5] - 2024-01-14 35 | 36 | ### Fixed 37 | 38 | - Fix bug triggered by OpenSSL 3.2.0 ([PR #105](https://github.com/ponylang/http/pull/105)) 39 | 40 | ## [0.5.4] - 2023-04-27 41 | 42 | ### Changed 43 | 44 | - Update ponylang/net_ssl dependency ([PR #88](https://github.com/ponylang/http/pull/88)) 45 | 46 | ## [0.5.3] - 2023-01-03 47 | 48 | ### Added 49 | 50 | - Add OpenSSL 3 support ([PR #85](https://github.com/ponylang/http/pull/85)) 51 | 52 | ## [0.5.2] - 2022-02-26 53 | 54 | ### Fixed 55 | 56 | - Update to work with Pony 0.49.0 ([PR #80](https://github.com/ponylang/http/pull/80)) 57 | 58 | ## [0.5.1] - 2022-02-10 59 | 60 | ### Added 61 | 62 | - Support for using ponyup on Windows ([PR #79](https://github.com/ponylang/http/pull/79)) 63 | 64 | ## [0.5.0] - 2022-02-02 65 | 66 | ### Changed 67 | 68 | - Update interfaces with private methods work with Pony 0.47.0 ([PR #76](https://github.com/ponylang/http/pull/76)) 69 | 70 | ## [0.4.5] - 2022-01-16 71 | 72 | ### Fixed 73 | 74 | - Update to work with Pony 0.46.0 ([PR #74](https://github.com/ponylang/http/pull/74)) 75 | 76 | ## [0.4.4] - 2021-09-03 77 | 78 | ### Fixed 79 | 80 | - Update to work with ponyc 0.44.0 ([PR #68](https://github.com/ponylang/http/pull/68)) 81 | 82 | ## [0.4.3] - 2021-09-01 83 | 84 | ### Added 85 | 86 | - Add Windows support ([PR #67](https://github.com/ponylang/http/pull/67)) 87 | 88 | ## [0.4.2] - 2021-07-30 89 | 90 | ### Fixed 91 | 92 | - Make Payload headers case insensitive ([PR #65](https://github.com/ponylang/http/pull/65)) 93 | 94 | ## [0.4.1] - 2021-05-05 95 | 96 | ### Changed 97 | 98 | - Update to match breaking API change in the pony standard library ([PR #63](https://github.com/ponylang/http/pull/63)) 99 | 100 | ## [0.4.0] - 2021-04-10 101 | 102 | ### Changed 103 | 104 | - Remove regex dependency ([PR #62](https://github.com/ponylang/http/pull/62)) 105 | 106 | ## [0.3.1] - 2021-04-10 107 | 108 | ### Changed 109 | 110 | - Update net_ssl dependency ([PR #61](https://github.com/ponylang/http/pull/61)) 111 | 112 | ## [0.3.0] - 2021-04-03 113 | 114 | ### Changed 115 | 116 | - Remove HTTP server code ([PR #57](https://github.com/ponylang/http/pull/57)) 117 | - Don't export test related classes ([PR #59](https://github.com/ponylang/http/pull/59)) 118 | 119 | ## [0.2.8] - 2021-03-10 120 | 121 | ### Fixed 122 | 123 | - Use buffered write for Payload - improves perf by ~200x ([PR #55](https://github.com/ponylang/http/pull/55)) 124 | 125 | ## [0.2.7] - 2021-02-08 126 | 127 | ## [0.2.6] - 2021-02-08 128 | 129 | ## [0.2.5] - 2020-05-06 130 | 131 | ### Changed 132 | 133 | - Switch dependency management from pony-stable to corral 134 | 135 | ## [0.2.4] - 2019-09-04 136 | 137 | ### Added 138 | 139 | - Automated release process 140 | - Changelog 141 | 142 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainers at coc@ponylang.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | 48 | # Social Rules 49 | 50 | In addition to having a code of conduct as an anti-harassment policy, we have a small set of [social rules](https://www.recurse.com/manual#sub-sec-social-rules) we follow. We (the project maintainers) lifted these rules from the [Recurse Center](https://www.recurse.com). We've seen these rules in effect in other environments. We'd like the Pony community to share a similar positive environment. These rules are intended to be lightweight, and to make more explicit certain social norms that are normally implicit. Most of our social rules really boil down to “don't be a jerk” or “don't be annoying.” Of course, almost nobody sets out to be a jerk or annoying, so telling people not to be jerks isn't a very productive strategy. 51 | 52 | Unlike the anti-harassment policy, violation of the social rules will not result in expulsion from the Pony community or a strong warning from project maintainers. Rather, they are designed to provide some lightweight social structure for community members to use when interacting with each other. 53 | 54 | ## No feigning surprise. 55 | 56 | The first rule means you shouldn't act surprised when people say they don't know something. This applies to both technical things ("What?! I can't believe you don't know what the stack is!") and non-technical things ("You don't know who RMS is?!"). Feigning surprise has absolutely no social or educational benefit: When people feign surprise, it's usually to make them feel better about themselves and others feel worse. And even when that's not the intention, it's almost always the effect. 57 | 58 | ## No well-actually's 59 | 60 | A well-actually happens when someone says something that's almost - but not entirely - correct, and you say, "well, actually…" and then give a minor correction. This is especially annoying when the correction has no bearing on the actual conversation. This doesn't mean we aren't about truth-seeking or that we don't care about being precise. Almost all well-actually's in our experience are about grandstanding, not truth-seeking. 61 | 62 | ## No subtle -isms 63 | 64 | Our last social rule bans subtle racism, sexism, homophobia, transphobia, and other kinds of bias. This one is different from the rest, because it covers a class of behaviors instead of one very specific pattern. 65 | 66 | Subtle -isms are small things that make others feel uncomfortable, things that we all sometimes do by mistake. For example, saying "It's so easy my grandmother could do it" is a subtle -ism. Like the other three social rules, this one is often accidentally broken. Like the other three, it's not a big deal to mess up – you just apologize and move on. 67 | 68 | If you see a subtle -ism in the Pony community, you can point it out to the relevant person, either publicly or privately, or you can ask one of the project maintainers to say something. After this, we ask that all further discussion move off of public channels. If you are a third party, and you don't see what could be biased about the comment that was made, feel free to talk to the project maintainers. Please don't say, "Comment X wasn't homophobic!" Similarly, please don't pile on to someone who made a mistake. The "subtle" in "subtle -isms" means that it's probably not obvious to everyone right away what was wrong with the comment. 69 | 70 | If you have any questions about any part of the code of conduct or social rules, please feel free to reach out to any of the project maintainers. 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You want to contribute to http? Awesome. 4 | 5 | There are a number of ways to contribute. As this document is a little long, feel free to jump to the section that applies to you currently: 6 | 7 | * [Bug report](#bug-report) 8 | * [How to contribute](#how-to-contribute) 9 | * [Pull request](#pull-request) 10 | 11 | Additional notes regarding formatting: 12 | 13 | * [Documentation formatting](#documentation-formatting) 14 | * [Code formatting](#code-formatting) 15 | * [File naming](#file-naming) 16 | 17 | ## Bug report 18 | 19 | First of all please [search existing issues](https://github.com/ponylang/http/issues) to make sure your issue hasn't already been reported. If you cannot find a suitable issue — [create a new one](https://github.com/ponylang/http/issues/new). 20 | 21 | Provide the following details: 22 | 23 | * short summary of what you were trying to achieve, 24 | * a code snippet causing the bug, 25 | * expected result, 26 | * actual results and 27 | * environment details: at least operating system version 28 | 29 | If possible, try to isolate the problem and provide just enough code to demonstrate it. Add any related information which might help to fix the issue. 30 | 31 | ## How to contribute 32 | 33 | This project uses a fairly standard GitHub pull request workflow. If you have already contributed to a project via GitHub pull request, you can skip this section and proceed to the [specific details of what we ask for in a pull request](#pull-request). If this is your first time contributing to a project via GitHub, read on. 34 | 35 | Here is the basic GitHub workflow: 36 | 37 | 1. Fork this repo. you can do this via the GitHub website. This will result in you having your own copy of the repo under your GitHub account. 38 | 2. Clone your forked repo to your local machine 39 | 3. Make a branch for your change 40 | 4. Make your change on that branch 41 | 5. Push your change to your repo 42 | 6. Use the github ui to open a PR 43 | 44 | Some things to note that aren't immediately obvious to folks just starting out: 45 | 46 | 1. Your fork doesn't automatically stay up to date with changes in the main repo. 47 | 2. Any changes you make on your branch that you used for one PR will automatically appear in another PR so if you have more than 1 PR, be sure to always create different branches for them. 48 | 3. Weird things happen with commit history if you don't create your PR branches off of `main` so always make sure you have the `main` branch checked out before creating a branch for a PR 49 | 50 | You can get help using GitHub via [the official documentation](https://help.github.com/). Some highlights include: 51 | 52 | * [Fork A Repo](https://help.github.com/articles/fork-a-repo/) 53 | * [Creating a pull request](https://help.github.com/articles/creating-a-pull-request/) 54 | * [Syncing a fork](https://help.github.com/articles/syncing-a-fork/) 55 | 56 | ## Pull request 57 | 58 | While we don't require that your pull request be a single commit, note that we will end up squashing all your commits into a single commit when we merge. While your PR is in review, we may ask for additional changes, please do not squash those commits while the review is underway. We ask that you not squash while a review is underway as it can make it hard to follow what is going on. 59 | 60 | When opening your pull request, please make sure that the initial comment on the PR is the commit message we should use when we merge. Making sure your commit message conforms to these guidelines for [writ(ing) a good commit message](http://chris.beams.io/posts/git-commit/). 61 | 62 | Make sure to issue 1 pull request per feature. Don't lump unrelated changes together. If you find yourself using the word "and" in your commit comment, you 63 | are probably doing too much for a single PR. 64 | 65 | We keep a [CHANGELOG](CHANGELOG.md) of all software changes with behavioural effects in ponyc. If your PR includes such changes (rather than say a documentation update), a Pony team member will do the following before merging it, so that the PR will be automatically added to the CHANGELOG: 66 | 67 | * Ensure that the ticket is tagged with one or more appropriate "changelog - *" labels - each label corresponds to a section of the changelog where this change will be automatically mentioned. 68 | * Ensure that the ticket title is appropriate - the title will be used as the summary of the change, so it should be appropriately formatted, including a ticket reference if the PR is a fix to an existing bug ticket. 69 | * For example, an appropriate title for a PR that fixes a bug reported in issue ticket #98 might look like: 70 | * *Fixed compiler crash related to tuple recovery (issue #98)* 71 | 72 | Once those conditions are met, the PR can be merged, and an automated system will immediately add the entry to the changelog. Keeping the changelog entries out of the file changes in the PR helps to avoid conflicts and other administrative headaches when many PRs are in progress. 73 | 74 | Any change that involves a changelog entry will trigger a bot to request that you add release notes to your PR. 75 | 76 | Pull requests from accounts that aren't members of the Ponylang organization require approval from a member before running. Approval is required after each update that you make. This could involve a lot of waiting on your part for approvals. If you are opening PRs to verify that changes all pass CI before "opening it for real", we strongly suggest that you open the PR against the `main` branch of your fork. CI will then run in your fork and you don't need to wait for approval from a Ponylang member. 77 | 78 | ## Documentation formatting 79 | 80 | When contributing to documentation, try to keep the following style guidelines in mind: 81 | 82 | As much as possible all documentation should be textual and in Markdown format. Diagrams are often needed to clarify a point. For any images, an original high-resolution source should be provided as well so updates can be made. 83 | 84 | Documentation is not "source code." As such, it should not be wrapped at 80 columns. Documentation should be allowed to flow naturally until the end of a paragraph. It is expected that the reader will turn on soft wrapping as needed. 85 | 86 | All code examples in documentation should be formatted in a fashion appropriate to the language in question. 87 | 88 | All command line examples in documentation should be presented in a copy and paste friendly fashion. Assume the user is using the `bash` shell. GitHub formatting on long command lines can be unfriendly to copy-and-paste. Long command lines should be broken up using `\` so that each line is no more than 80 columns. Wrapping at 80 columns should result in a good display experience in GitHub. Additionally, continuation lines should be indented two spaces. 89 | 90 | OK: 91 | 92 | ```bash 93 | my_command --some-option foo --path-to-file ../../project/long/line/foo \ 94 | --some-other-option bar 95 | ``` 96 | 97 | Not OK: 98 | 99 | ```bash 100 | my_command --some-option foo --path-to-file ../../project/long/line/foo --some-other-option bar 101 | ``` 102 | 103 | Wherever possible when writing documentation, favor full command options rather than short versions. Full flags are usually much easier to modify because the meaning is clearer. 104 | 105 | OK: 106 | 107 | ```bash 108 | my_command --messages 100 109 | ``` 110 | 111 | Not OK: 112 | 113 | ```bash 114 | my_command -m 100 115 | ``` 116 | 117 | ## Code formatting 118 | 119 | The basics: 120 | 121 | * Indentation 122 | 123 | Indent using spaces, not tabs. Indentation is language specific. 124 | 125 | * Watch your whitespace! 126 | 127 | Use an editor plugin to remove unused trailing whitespace including both at the end of a line and at the end of a file. By the same token, remember to leave a single newline only line at the end of each file. It makes output files to the console much more pleasant. 128 | 129 | * Line Length 130 | 131 | We all have different sized monitors. What might look good on yours might look like awful on another. Be kind and wrap all lines at 80 columns unless you have a good reason not to. 132 | 133 | * Reformatting code to meet standards 134 | 135 | Try to avoid doing it. A commit that changes the formatting for large chunks of a file makes for an ugly commit history when looking for changes. Don't commit code that doesn't conform to coding standards in the first place. If you do reformat code, make sure it is either standalone reformatting with no logic changes or confined solely to code whose logic you touched. For example, updating the indentation in a file? Do not make logic changes along with it. Editing a line that has extra whitespace at the end? Feel free to remove it. 136 | 137 | The details: 138 | 139 | All Pony sources should follow the [Pony standard library style guide](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md). 140 | 141 | ## File naming 142 | 143 | Pony code follows the [Pony standard library file naming guidelines](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md#naming). 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018, The Pony Developers 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | config ?= release 2 | 3 | PACKAGE := http 4 | GET_DEPENDENCIES_WITH := corral fetch 5 | CLEAN_DEPENDENCIES_WITH := corral clean 6 | COMPILE_WITH := corral run -- ponyc 7 | 8 | BUILD_DIR ?= build/$(config) 9 | SRC_DIR ?= $(PACKAGE) 10 | EXAMPLES_DIR := examples 11 | BENCH_DIR := bench 12 | tests_binary := $(BUILD_DIR)/$(PACKAGE) 13 | bench_binary := $(BUILD_DIR)/bench 14 | docs_dir := build/$(PACKAGE)-docs 15 | 16 | ifdef config 17 | ifeq (,$(filter $(config),debug release)) 18 | $(error Unknown configuration "$(config)") 19 | endif 20 | endif 21 | 22 | ifeq ($(config),release) 23 | PONYC = $(COMPILE_WITH) 24 | else 25 | PONYC = $(COMPILE_WITH) --debug 26 | endif 27 | 28 | ifeq (,$(filter $(MAKECMDGOALS),clean docs realclean TAGS)) 29 | ifeq ($(ssl), 3.0.x) 30 | SSL = -Dopenssl_3.0.x 31 | else ifeq ($(ssl), 1.1.x) 32 | SSL = -Dopenssl_1.1.x 33 | else ifeq ($(ssl), 0.9.0) 34 | SSL = -Dopenssl_0.9.0 35 | else 36 | $(error Unknown SSL version "$(ssl)". Must set using 'ssl=FOO') 37 | endif 38 | endif 39 | 40 | PONYC := $(PONYC) $(SSL) 41 | 42 | SOURCE_FILES := $(shell find $(SRC_DIR) -name *.pony) 43 | EXAMPLES := $(notdir $(shell find $(EXAMPLES_DIR)/* -type d)) 44 | EXAMPLES_SOURCE_FILES := $(shell find $(EXAMPLES_DIR) -name *.pony) 45 | EXAMPLES_BINARIES := $(addprefix $(BUILD_DIR)/,$(EXAMPLES)) 46 | BENCH_SOURCE_FILES := $(shell find $(BENCH_DIR) -name *.pony) 47 | 48 | test: unit-tests build-examples 49 | 50 | unit-tests: $(tests_binary) 51 | $^ --exclude=integration --sequential 52 | 53 | $(tests_binary): $(SOURCE_FILES) | $(BUILD_DIR) 54 | $(GET_DEPENDENCIES_WITH) 55 | $(PONYC) -o $(BUILD_DIR) $(SRC_DIR) 56 | 57 | build-examples: $(EXAMPLES_BINARIES) 58 | 59 | $(EXAMPLES_BINARIES): $(BUILD_DIR)/%: $(SOURCE_FILES) $(EXAMPLES_SOURCE_FILES) | $(BUILD_DIR) 60 | $(GET_DEPENDENCIES_WITH) 61 | $(PONYC) -o $(BUILD_DIR) $(EXAMPLES_DIR)/$* 62 | 63 | clean: 64 | $(CLEAN_DEPENDENCIES_WITH) 65 | rm -rf $(BUILD_DIR) 66 | 67 | $(docs_dir): $(SOURCE_FILES) 68 | rm -rf $(docs_dir) 69 | $(GET_DEPENDENCIES_WITH) 70 | $(PONYC) --docs-public --pass=docs --output build $(SRC_DIR) 71 | 72 | docs: $(docs_dir) 73 | 74 | $(bench_binary): $(SOURCE_FILES) $(BENCH_SOURCE_FILES) | $(BUILD_DIR) 75 | $(GET_DEPENDENCIES_WITH) 76 | $(PONYC) $(BENCH_DIR) -o $(BUILD_DIR) 77 | 78 | bench: $(bench_binary) 79 | $(bench_binary) 80 | 81 | .coverage: 82 | mkdir -p .coverage 83 | 84 | coverage: .coverage $(tests_binary) 85 | kcov --include-pattern="$(SRC_DIR)" --exclude-pattern="*/test/*.pony,*/_test.pony" .coverage $(tests_binary) 86 | 87 | TAGS: 88 | ctags --recurse=yes $(SRC_DIR) 89 | 90 | all: test 91 | 92 | $(BUILD_DIR): 93 | mkdir -p $(BUILD_DIR) 94 | 95 | .PHONY: all build-examples clean TAGS test 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http 2 | 3 | Ponylang package to build clients for the HTTP protocol. 4 | 5 | ## Status 6 | 7 | `http` is beta quality software that will change frequently. Expect breaking changes. That said, you should feel comfortable using it in your projects. 8 | 9 | ## Installation 10 | 11 | * Add `http` to your build dependencies using [corral](https://github.com/ponylang/corral): 12 | 13 | ```bash 14 | corral add github.com/ponylang/http.git --version 0.6.2 15 | ``` 16 | 17 | * Execute `corral fetch` to fetch your dependencies. 18 | * Include this package by adding `use "http"` to your Pony sources. 19 | * Execute `corral run -- ponyc` to compile your application 20 | 21 | Note: The `net-ssl` transitive dependency requires a C SSL library to be installed. Please see the [net-ssl installation instructions](https://github.com/ponylang/net-ssl#installation) for more information. 22 | 23 | ## API Documentation 24 | 25 | [https://ponylang.github.io/http](https://ponylang.github.io/http) 26 | -------------------------------------------------------------------------------- /RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # How to cut a http release 2 | 3 | This document is aimed at members of the Pony team who might be cutting a release of Pony. It serves as a checklist that can take you through doing a release step-by-step. 4 | 5 | ## Prerequisites 6 | 7 | You must have commit access to the http repository 8 | 9 | ## Releasing 10 | 11 | Please note that this document was written with the assumption that you are using a clone of the `http` repo. You have to be using a clone rather than a fork. It is advised to your do this by making a fresh clone of the `http` repo from which you will release. 12 | 13 | ```bash 14 | git clone git@github.com:ponylang/http.git http-release-clean 15 | cd http-release-clean 16 | ``` 17 | 18 | Before getting started, you will need a number for the version that you will be releasing as well as an agreed upon "golden commit" that will form the basis of the release. 19 | 20 | The "golden commit" must be `HEAD` on the `main` branch of this repository. At this time, releasing from any other location is not supported. 21 | 22 | For the duration of this document, that we are releasing version is `0.3.1`. Any place you see those values, please substitute your own version. 23 | 24 | ```bash 25 | git tag release-0.3.1 26 | git push origin release-0.3.1 27 | ``` 28 | -------------------------------------------------------------------------------- /STYLE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Style Guide 2 | 3 | http follows the [Pony standard library Style Guide](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md). 4 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.2 2 | -------------------------------------------------------------------------------- /bench/bench.pony: -------------------------------------------------------------------------------- 1 | use "../http" 2 | use "buffered" 3 | use "pony_bench" 4 | use "debug" 5 | use "format" 6 | 7 | actor Main is BenchmarkList 8 | new create(env: Env) => 9 | PonyBench(env, this) 10 | 11 | fun tag benchmarks(bench: PonyBench) => 12 | bench(_SimpleGetRequestBenchmark) 13 | bench(_FormSubmissionRequestBenchmark) 14 | bench(_SplitFormSubmissionRequestBenchmark) 15 | bench(_MultipartFileUploadBenchmark) 16 | bench(_ChunkedRequestBenchmark) 17 | 18 | actor _TestHTTPSession is HTTPSession 19 | var _c: (AsyncBenchContinue | None) = None 20 | 21 | be set_continue(c: AsyncBenchContinue) => 22 | _c = c 23 | 24 | be apply(payload: Payload val) => 25 | Debug("apply") 26 | 27 | be finish() => 28 | Debug("finish") 29 | 30 | be dispose() => 31 | Debug("dispose") 32 | 33 | be write(data: ByteSeq val) => None 34 | 35 | be cancel(msg: Payload val) => None 36 | 37 | be _mute() => None 38 | 39 | be _unmute() => None 40 | 41 | be _deliver(payload: Payload val) => 42 | Debug("_deliver") 43 | match payload.transfer_mode 44 | | OneshotTransfer => 45 | try 46 | (_c as AsyncBenchContinue).complete() 47 | else 48 | Debug("no benchcontinue set") 49 | end 50 | else 51 | Debug("_deliver chunk|stream") 52 | end 53 | 54 | be _chunk(data: ByteSeq val) => 55 | Debug("_chunk") 56 | 57 | be _finish() => None 58 | Debug("_finish") 59 | try 60 | (_c as AsyncBenchContinue).complete() 61 | else 62 | Debug("_finish None") 63 | end 64 | 65 | class _ParseRequestBenchmark 66 | let _data: Array[String] 67 | let _reader: Reader = Reader 68 | let _session: _TestHTTPSession = _TestHTTPSession.create() 69 | let _parser: HTTPParser = HTTPParser.request(_session) 70 | 71 | new create(data: Array[String]) => 72 | _data = data 73 | 74 | fun ref apply(c: AsyncBenchContinue) ? => 75 | _session.set_continue(c) 76 | _parser.restart() 77 | _reader.clear() 78 | let data_iter = _data.values() 79 | while data_iter.has_next() do 80 | let chunk = data_iter.next()? 81 | _reader.append(chunk) 82 | match _parser.parse(_reader) 83 | | ParseError => 84 | Debug("parsing failed.") 85 | if not data_iter.has_next() then 86 | c.fail() 87 | end 88 | end 89 | end 90 | 91 | class iso _SimpleGetRequestBenchmark is AsyncMicroBenchmark 92 | 93 | let data: Array[String] = [ 94 | "\r\n".join( 95 | [ 96 | "GET /get HTTP/1.1" 97 | "Host: httpbin.org" 98 | "User-Agent: curl/7.58.0" 99 | "Accept: */*" 100 | "" 101 | "" 102 | ].values()) 103 | ] 104 | 105 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 106 | 107 | fun config(): BenchConfig => BenchConfig( 108 | where max_iterations' = 100) 109 | 110 | fun name(): String => "request/simple" 111 | 112 | fun ref apply(c: AsyncBenchContinue)? => 113 | Debug("running bench") 114 | _bench.apply(c)? 115 | 116 | 117 | class iso _FormSubmissionRequestBenchmark is AsyncMicroBenchmark 118 | let data: Array[String] = [ 119 | "\r\n".join([ 120 | "POST /post HTTP/1.1" 121 | "Host: httpbin.org" 122 | "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0" 123 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 124 | "Accept-Language: en-GB,en;q=0.5" 125 | "Accept-Encoding: gzip, deflate" 126 | "Referer: http://httpbin.org/forms/post" 127 | "Content-Type: application/x-www-form-urlencoded" 128 | "Content-Length: 174" 129 | "Cookie: _gauges_unique_hour=1; _gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1" 130 | "Connection: keep-alive" 131 | "Upgrade-Insecure-Requests: 1" 132 | "" 133 | "custname=Pony+Mc+Ponyface&custtel=%2B490123456789&custemail=pony%40ponylang.org&size=large&topping=bacon&topping=cheese&topping=onion&delivery=&comments=This+is+a+stupid+test" 134 | ].values()) 135 | ] 136 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 137 | 138 | fun config(): BenchConfig => BenchConfig( 139 | where max_iterations' = 100) 140 | 141 | fun name(): String => "request/form-submission" 142 | 143 | fun ref apply(c: AsyncBenchContinue)? => 144 | _bench.apply(c)? 145 | 146 | class iso _SplitFormSubmissionRequestBenchmark is AsyncMicroBenchmark 147 | let data: Array[String] = [ 148 | "\r\n".join([ 149 | "POST /post HTTP/1.1" 150 | "Host: httpbin.org" 151 | "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0" 152 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 153 | "Accept-Language: en-GB,en;q=0.5" 154 | "Accept-Encoding: gzip, deflate" 155 | "Referer: http://httpbin.org/forms/post" 156 | "Content-Type: application/x-www-form-urlencoded" 157 | "Content-Length: 174" 158 | "Cookie: _gauges_unique_hour=1; _gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1" 159 | "Connection: keep-alive" 160 | "Upgrade-Insecure-Req" 161 | ].values()) 162 | "\r\n".join([ 163 | "uests: 1" 164 | "" 165 | "custname=Pony+Mc+Ponyface&custtel=%2B490123456789&custemail=pony%40ponylang.org&size=large&topping=bacon&topping=cheese&topping=onion&delivery=&comments=This+is+a+stupid+test" 166 | ].values()) 167 | ] 168 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 169 | 170 | fun config(): BenchConfig => BenchConfig( 171 | where max_iterations' = 100) 172 | 173 | fun name(): String => "request/form-submission/split" 174 | 175 | fun ref apply(c: AsyncBenchContinue)? => 176 | _bench.apply(c)? 177 | 178 | class iso _MultipartFileUploadBenchmark is AsyncMicroBenchmark 179 | let data: Array[String] = [ 180 | "\r\n".join([ 181 | "POST /cgi-bin/request HTTP/1.1" 182 | "Host: localhost" 183 | "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0" 184 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 185 | "Accept-Language: en-GB,en;q=0.5" 186 | "Accept-Encoding: gzip, deflate" 187 | "Connection: keep-alive" 188 | "Content-Type: multipart/form-data; boundary=abcdef123456" 189 | "Content-Length: 10001" // forcing streaming 190 | "" 191 | "--abcdef123456" 192 | "Content-Disposition: form-data; name=\"random_stuff1\"" 193 | String.from_array(recover val Array[U8].init('a', 5000) end) 194 | "Content-Disposition: form-data; name=\"random_stuff2\"" 195 | String.from_array(recover val Array[U8].init('b', 4867) end) 196 | "--abcdef123456--" 197 | ].values())] 198 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 199 | 200 | fun config(): BenchConfig => BenchConfig( 201 | where max_iterations' = 100) 202 | 203 | fun name(): String => "request/multipart-file-upload" 204 | 205 | fun ref apply(c: AsyncBenchContinue)? => 206 | _bench.apply(c)? 207 | 208 | class iso _ChunkedRequestBenchmark is AsyncMicroBenchmark 209 | let data: Array[String] = [ 210 | "\r\n".join([ 211 | "GET /get HTTP/1.1" 212 | "Host: localhost:8888" 213 | "User-Agent: curl/7.58.0" 214 | "Accept: */*" 215 | "Transfer-Encoding: chunked" 216 | "Content-Type: application/x-www-form-urlencoded" 217 | "" 218 | Format.int[U64](100 where fmt=FormatHexBare) 219 | String.from_array(recover val Array[U8].init('a', 100) end) 220 | Format.int[U64](500 where fmt=FormatHexBare) 221 | String.from_array(recover val Array[U8].init('b', 500) end) 222 | "0" 223 | "" 224 | "" 225 | ].values()) 226 | ] 227 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 228 | 229 | fun config(): BenchConfig => BenchConfig( 230 | where max_iterations' = 100) 231 | 232 | fun name(): String => "request/chunked" 233 | 234 | fun ref apply(c: AsyncBenchContinue)? => 235 | _bench.apply(c)? 236 | 237 | -------------------------------------------------------------------------------- /corral.json: -------------------------------------------------------------------------------- 1 | { 2 | "deps": [ 3 | { 4 | "locator": "github.com/ponylang/net_ssl.git", 5 | "version": "1.3.3" 6 | } 7 | ], 8 | "packages": [ 9 | "http" 10 | ], 11 | "info": { 12 | "description": "HTTP client library", 13 | "homepage": "https://github.com/ponylang/http", 14 | "license": "BSD-2-Clause", 15 | "documentation_url": "https://ponylang.github.io/http/", 16 | "version": "0.6.2", 17 | "name": "http" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/httpget/httpget.pony: -------------------------------------------------------------------------------- 1 | use "assert" 2 | use "cli" 3 | use "collections" 4 | use "encode/base64" 5 | use "files" 6 | use "../../http" 7 | use "net_ssl" 8 | use "net" 9 | 10 | class val Config 11 | let user: String 12 | let pass: String 13 | let output: String 14 | let url: String 15 | let timeout: U64 16 | 17 | new val create(env: Env) ? => 18 | let cs = CommandSpec.leaf("httpget", "", [ 19 | OptionSpec.string("user", "Username for authenticated queries." 20 | where short' = 'u', default' = "") 21 | OptionSpec.string("pass", "Password for authenticated queries." 22 | where short' = 'p', default' = "") 23 | OptionSpec.string("output", "Name of file to write response body." 24 | where short' = 'o', default' = "") 25 | OptionSpec.u64("timeout", "TCP Keepalive timeout to detect broken communications link." 26 | where short' = 't', default' = U64(0)) 27 | ],[ 28 | ArgSpec.string("url", "Url to query." where default' = None) 29 | ])?.>add_help()? 30 | let cmd = 31 | match CommandParser(cs).parse(env.args, env.vars) 32 | | let c: Command => c 33 | | let ch: CommandHelp => 34 | ch.print_help(env.out) 35 | env.exitcode(0) 36 | error 37 | | let se: SyntaxError => 38 | env.out.print(se.string()) 39 | env.exitcode(1) 40 | error 41 | end 42 | user = cmd.option("user").string() 43 | pass = cmd.option("pass").string() 44 | output = cmd.option("output").string() 45 | url = cmd.arg("url").string() 46 | timeout = cmd.option("timeout").u64() 47 | 48 | actor Main 49 | """ 50 | Fetch data from URLs on the command line. 51 | """ 52 | new create(env: Env) => 53 | // Get common command line options. 54 | let c = try Config(env)? else return end 55 | 56 | let url = try 57 | URL.valid(c.url)? 58 | else 59 | env.out.print("Invalid URL: " + c.url) 60 | env.exitcode(1) 61 | return 62 | end 63 | 64 | // Start the actor that does the real work. 65 | _GetWork.create(env, url, c.user, c.pass, c.output, c.timeout) 66 | 67 | actor _GetWork 68 | """ 69 | Do the work of fetching a resource 70 | """ 71 | let _env: Env 72 | 73 | new create(env: Env, url: URL, user: String, pass: String, output: String, timeout: U64) 74 | => 75 | """ 76 | Create the worker actor. 77 | """ 78 | _env = env 79 | 80 | // Get certificate for HTTPS links. 81 | 82 | let sslctx = try 83 | recover 84 | SSLContext 85 | .>set_client_verify(true) 86 | .>set_authority(FilePath(FileAuth(env.root), "cacert.pem"))? 87 | end 88 | else 89 | env.out.print("Unable to create cert.") 90 | env.exitcode(1) 91 | end 92 | 93 | // The Notify Factory will create HTTPHandlers as required. It is 94 | // done this way because we do not know exactly when an HTTPSession 95 | // is created - they can be re-used. 96 | let dumpMaker = recover val NotifyFactory.create(this) end 97 | 98 | // The Client manages all links. 99 | let client = HTTPClient( 100 | TCPConnectAuth(env.root), 101 | dumpMaker, 102 | consume sslctx 103 | where keepalive_timeout_secs = timeout.u32() 104 | ) 105 | 106 | try 107 | // Start building a GET request. 108 | let req = Payload.request("GET", url) 109 | req("User-Agent") = "Pony httpget" 110 | 111 | // Add authentication if supplied. We use the "Basic" format, 112 | // which is username:password in base64. In a real example, 113 | // you would only use this on an https link. 114 | if user.size() > 0 then 115 | let keyword = "Basic " 116 | let content = recover String(user.size() + pass.size() + 1) end 117 | content.append(user) 118 | content.append(":") 119 | content.append(pass) 120 | let coded = Base64.encode(consume content) 121 | let auth = recover String(keyword.size() + coded.size()) end 122 | auth.append(keyword) 123 | auth.append(consume coded) 124 | req("Authorization") = consume auth 125 | end 126 | 127 | // Submit the request 128 | let sentreq = client(consume req)? 129 | 130 | // Could send body data via `sentreq`, if it was a POST 131 | else 132 | try env.out.print("Malformed URL: " + env.args(1)?) end 133 | env.exitcode(1) 134 | end 135 | 136 | be cancelled() => 137 | """ 138 | Process cancellation from the server end. 139 | """ 140 | _env.out.print("-- response cancelled --") 141 | 142 | be failed(reason: HTTPFailureReason) => 143 | match reason 144 | | AuthFailed => 145 | _env.err.print("-- auth failed --") 146 | | ConnectFailed => 147 | _env.err.print("-- connect failed --") 148 | | ConnectionClosed => 149 | _env.err.print("-- connection closed --") 150 | end 151 | _env.exitcode(1) 152 | 153 | be have_response(response: Payload val) => 154 | """ 155 | Process return the the response message. 156 | """ 157 | if response.status == 0 then 158 | _env.out.print("Failed") 159 | _env.exitcode(1) 160 | return 161 | end 162 | 163 | // Print the status and method 164 | _env.out.print( 165 | "Response " + 166 | response.status.string() + " " + 167 | response.method) 168 | 169 | // Print all the headers 170 | for (k, v) in response.headers().pairs() do 171 | _env.out.print(k + ": " + v) 172 | end 173 | 174 | _env.out.print("") 175 | 176 | // Print the body if there is any. This will fail in Chunked or 177 | // Stream transfer modes. 178 | try 179 | let body = response.body()? 180 | for piece in body.values() do 181 | _env.out.write(piece) 182 | end 183 | end 184 | 185 | be have_body(data: ByteSeq val) 186 | => 187 | """ 188 | Some additional response data. 189 | """ 190 | _env.out.write(data) 191 | 192 | be finished() => 193 | """ 194 | End of the response data. 195 | """ 196 | _env.out.print("-- end of body --") 197 | 198 | class NotifyFactory is HandlerFactory 199 | """ 200 | Create instances of our simple Receive Handler. 201 | """ 202 | let _main: _GetWork 203 | 204 | new iso create(main': _GetWork) => 205 | _main = main' 206 | 207 | fun apply(session: HTTPSession): HTTPHandler ref^ => 208 | HttpNotify.create(_main, session) 209 | 210 | class HttpNotify is HTTPHandler 211 | """ 212 | Handle the arrival of responses from the HTTP server. These methods are 213 | called within the context of the HTTPSession actor. 214 | """ 215 | let _main: _GetWork 216 | let _session: HTTPSession 217 | 218 | new ref create(main': _GetWork, session: HTTPSession) => 219 | _main = main' 220 | _session = session 221 | 222 | fun ref apply(response: Payload val) => 223 | """ 224 | Start receiving a response. We get the status and headers. Body data 225 | *might* be available. 226 | """ 227 | _main.have_response(response) 228 | 229 | fun ref chunk(data: ByteSeq val) => 230 | """ 231 | Receive additional arbitrary-length response body data. 232 | """ 233 | _main.have_body(data) 234 | 235 | fun ref finished() => 236 | """ 237 | This marks the end of the received body data. We are done with the 238 | session. 239 | """ 240 | _main.finished() 241 | _session.dispose() 242 | 243 | fun ref cancelled() => 244 | _main.cancelled() 245 | 246 | fun ref failed(reason: HTTPFailureReason) => 247 | _main.failed(reason) 248 | -------------------------------------------------------------------------------- /http/_client_conn_handler.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use "net" 3 | 4 | class _ClientConnHandler is TCPConnectionNotify 5 | """ 6 | This is the network notification handler for the client. It passes 7 | received data to the `HTTPParser` to assemble response `Payload` objects. 8 | """ 9 | let _session: _ClientConnection 10 | let _keepalive_timeout_secs: U32 11 | let _buffer: Reader = Reader 12 | let _parser: HTTPParser 13 | var _delivered: Bool = false 14 | 15 | new iso create( 16 | client: _ClientConnection, 17 | keepalive_timeout_secs: U32 = 0) => 18 | """ 19 | The response builder needs to know which Session to forward 20 | parsed information to. 21 | """ 22 | _session = client 23 | _keepalive_timeout_secs = keepalive_timeout_secs 24 | _parser = HTTPParser.response(_session) 25 | 26 | fun ref connected(conn: TCPConnection ref) => 27 | """ 28 | Tell the client we have connected. 29 | 30 | And set the desired keepalive timeout (or disable it, which is the default). 31 | """ 32 | conn.set_keepalive(_keepalive_timeout_secs) 33 | _session._connected(conn) 34 | 35 | fun ref connect_failed(conn: TCPConnection ref) => 36 | """ 37 | The connection could not be established. Tell the client not to proceed. 38 | """ 39 | _session._connect_failed(conn) 40 | 41 | fun ref auth_failed(conn: TCPConnection ref) => 42 | """ 43 | SSL authentication failed. Tell the client not to proceed. 44 | """ 45 | _session._auth_failed(conn) 46 | 47 | fun ref received(conn: TCPConnection ref, data: Array[U8] iso, 48 | times: USize): Bool 49 | => 50 | """ 51 | Pass a received chunk of data to the `HTTPParser`. 52 | """ 53 | // TODO: inactivity timer 54 | _buffer.append(consume data) 55 | 56 | // Let the parser take a look at what has been received. 57 | match _parser.parse(_buffer) 58 | // Any syntax errors will terminate the connection. 59 | | ParseError => conn.close() 60 | end 61 | true 62 | 63 | fun ref closed(conn: TCPConnection ref) => 64 | """ 65 | The connection has closed, possibly prematurely. 66 | """ 67 | _parser.closed(_buffer) 68 | _buffer.clear() 69 | _session._closed(conn) 70 | 71 | fun ref throttled(conn: TCPConnection ref) => 72 | """ 73 | TCP connection wants us to stop sending. We do not do anything with 74 | this here; just pass it on to the `HTTPSession`. 75 | """ 76 | _session.throttled() 77 | 78 | fun ref unthrottle(conn: TCPConnection ref) => 79 | """ 80 | TCP can accept more data now. We just pass this on to the 81 | `HTTPSession` 82 | """ 83 | _session.unthrottled() 84 | 85 | -------------------------------------------------------------------------------- /http/_client_connection.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "net" 3 | use "net_ssl" 4 | use "buffered" 5 | 6 | primitive _ConnConnecting 7 | 8 | actor _ClientConnection is HTTPSession 9 | """ 10 | Manages a persistent and possibly pipelined TCP connection to an HTTP server. 11 | 12 | This is where pipelining happens, if it has been enabled by the `Client`. 13 | Only "safe" requests (GET, HEAD, OPTIONS) are sent to the server if 14 | *any* responses are still pending. 15 | 16 | The `HTTPHandler.need_body` notification function will be called if/when 17 | the `HTTPSession` is ready to receive body data for `POST` operations in 18 | transfer modes other than `Oneshot`. 19 | 20 | ### Receiving Responses 21 | 22 | Reception is handled through an `HTTPHandler` instance. 23 | `HTTPHandler.apply` signals the arrival of a message with headers. 24 | How the body data is obtained depends on the `transfer` mode. 25 | 26 | * For `StreamTranfer` and `ChunkedTransfer` modes, there will be 27 | any number of `HTTPHandler.chunk` notifications, followed by 28 | an `HTTPHandler.finished` notification. 29 | 30 | * For `OneShotTransfer` mode, the client application just needs to 31 | call `Payload.body` once to get the entire body. 32 | 33 | ## The HandlerFactory 34 | 35 | The `Client` class will try to re-use sessions. If it needs to create 36 | a new session, based on the request URL, it will do that, and then it 37 | will need a new instance of the caller's `HTTPHandler` class. 38 | Since the client application code does not know in advance when this 39 | will be necessary, it passes in a `HandlerFactory` that creates the 40 | actual `HTTPHandler`, customized 41 | for the client application's needs. 42 | """ 43 | let _auth: TCPConnectAuth 44 | let _host: String 45 | let _service: String 46 | let _sslctx: (SSLContext | None) 47 | let _pipeline: Bool 48 | let _keepalive_timeout_secs: U32 49 | let _app_handler: HTTPHandler 50 | let _unsent: List[Payload val] = _unsent.create() 51 | let _sent: List[Payload val] = _sent.create() 52 | var _safewait: Bool = false 53 | var _conn: (TCPConnection | None | _ConnConnecting) = None 54 | var _nobackpressure: Bool = true // TCP backpressure indicator 55 | embed _wr: Writer = Writer 56 | 57 | new create( 58 | auth: TCPConnectAuth, 59 | host: String, 60 | service: String, 61 | sslctx: (SSLContext | None) = None, 62 | pipeline: Bool = true, 63 | keepalive_timeout_secs: U32 = 0, 64 | handlermaker: HandlerFactory val) 65 | => 66 | """ 67 | Create a connection for the given host and service. We also create 68 | an instance of the client application's HTTPHandler. 69 | """ 70 | _auth = auth 71 | _host = host 72 | _service = service 73 | _sslctx = sslctx 74 | _pipeline = pipeline 75 | _keepalive_timeout_secs = keepalive_timeout_secs 76 | _app_handler = handlermaker(this) 77 | 78 | be apply(request: Payload val) => 79 | """ 80 | Schedule a request to be sent by adding it to the `unsent` queue 81 | for this session. 82 | """ 83 | _unsent.push(consume request) 84 | _send_pending() 85 | 86 | be cancel(request: Payload val) => 87 | """ 88 | Cancel a request. 89 | """ 90 | // We look for it first in the unsent queue. If it is there, 91 | // we just remove it. 92 | try 93 | for node in _unsent.nodes() do 94 | if node()? is request then 95 | node .> remove().pop()? 96 | _app_handler.cancelled() 97 | return 98 | end 99 | end 100 | 101 | // It might have been sent already, but no response received 102 | // yet. In that case we have to close the connection so that 103 | // the server finds out. 104 | for node in _sent.nodes() do 105 | if node()? is request then 106 | try (_conn as TCPConnection).dispose() end 107 | _conn = None 108 | node .> remove().pop()? 109 | _app_handler.cancelled() 110 | break 111 | end 112 | end 113 | end 114 | 115 | be _deliver(response: Payload val) => 116 | """ 117 | Deal with a new Response coming back from the server. 118 | 119 | Since the session operates in a FIFO manner, the Request corresponding 120 | to this Response is the oldest one on the `_sent` list. We take it 121 | off that list and call its handler. It becomes the 'currently being 122 | delivered' response and subsequent body data has to go there as well, 123 | if there is any. 124 | """ 125 | try 126 | let request = _sent.shift()? 127 | _app_handler(response) 128 | 129 | // If that request has no body data coming, we can go look 130 | // for more requests to send. 131 | if response.transfer_mode is OneshotTransfer then 132 | _send_pending() 133 | end 134 | end 135 | 136 | be _connected(conn: TCPConnection) => 137 | """ 138 | The connection to the server has been established. Send pending requests. 139 | """ 140 | _nobackpressure = true 141 | _conn = conn 142 | _send_pending() 143 | 144 | be _connect_failed(conn: TCPConnection) => 145 | """ 146 | The connection couldn't be established. Cancel all pending requests. 147 | """ 148 | _cancel_all() 149 | _conn = None 150 | _app_handler.failed(ConnectFailed) 151 | 152 | be _auth_failed(conn: TCPConnection) => 153 | """ 154 | The connection couldn't be authenticated. Cancel all pending requests. 155 | """ 156 | _cancel_all() 157 | _conn = None 158 | _app_handler.failed(AuthFailed) 159 | 160 | be _closed(conn: TCPConnection) => 161 | """ 162 | The connection to the server has closed prematurely. Cancel everything. 163 | """ 164 | if conn is _conn then 165 | _cancel_all() 166 | _conn = None 167 | _app_handler.failed(ConnectionClosed) 168 | end 169 | 170 | be write(data: ByteSeq val) => 171 | """ 172 | Write a low-level byte stream. The `Payload` objects call this to 173 | generate their wire representation. 174 | """ 175 | match _conn 176 | | let c: TCPConnection => c.write(data) 177 | end 178 | 179 | be _chunk(data: ByteSeq val) => 180 | """ 181 | Called when *inbound* body data has arrived for the currently 182 | inbound `Payload`. This should be passed directly to the application's 183 | `HTTPHandler.chunk` method. 184 | """ 185 | _app_handler.chunk(data) 186 | 187 | be _finish() => 188 | """ 189 | Indicates that the last *inbound* body chunk has been sent to 190 | `_chunk`. This is passed on to the front end. 191 | 192 | _send_pending is called to detect that _unsent and _sent are emptye 193 | and that _conn can be disposed. 194 | """ 195 | _app_handler.finished() 196 | _send_pending() 197 | 198 | be finish() => 199 | """ 200 | We are done sending a request with a long body. 201 | """ 202 | None 203 | 204 | be dispose() => 205 | """ 206 | Cancels all requests and disposes the tcp connection. 207 | """ 208 | if _cancel_all() then 209 | _app_handler.cancelled() 210 | end 211 | match _conn 212 | | let c: TCPConnection => c.dispose() 213 | end 214 | _conn = None 215 | 216 | be throttled() => 217 | """ 218 | The connection to the server can not accept data for a while. 219 | We set a local flag too so we do not send anything on the queue. 220 | """ 221 | _nobackpressure = false 222 | _app_handler.throttled() 223 | 224 | be unthrottled() => 225 | """ 226 | The connection to the server can now accept more data. 227 | """ 228 | _nobackpressure = true 229 | _app_handler.unthrottled() 230 | _send_pending() 231 | 232 | fun ref _send_pending() => 233 | """ 234 | Send pending requests to the server. If the connection is closed, 235 | open it. If we have nothing to send and we aren't waiting on any 236 | responses, close the connection. 237 | """ 238 | if _unsent.size() == 0 then 239 | if _sent.size() == 0 then 240 | try 241 | (_conn as TCPConnection).dispose() 242 | _conn = None 243 | end 244 | end 245 | return 246 | end 247 | 248 | // If waiting for response to an unsafe request, do not send more requests. 249 | // TODO this check has to be in Client so that the apply fails. 250 | if _safewait then return end 251 | 252 | try 253 | // Get the existing connection, if it is there. 254 | let conn = _conn as TCPConnection 255 | 256 | try 257 | // Send requests until backpressure makes us stop, or we 258 | // send an unsafe request. 259 | while _nobackpressure do 260 | // Take a request off the unsent queue and notice whether 261 | // it is safe. 262 | let request = _unsent.shift()? 263 | let safereq = request.is_safe() 264 | // Send all of the request that is possible for now. 265 | request._write(where wr = _wr, keepalive = true) 266 | conn.writev(_wr.done()) 267 | 268 | // If there is a follow-on body, tell client to send it now. 269 | if request.has_body() then 270 | match request.transfer_mode 271 | | OneshotTransfer => finish() 272 | else 273 | _app_handler.need_body() 274 | end 275 | else 276 | finish() 277 | end 278 | 279 | // Put the request on the list of things we are waiting for. 280 | _sent.push(consume request) 281 | 282 | // If it was not 'safe', send no more for now. 283 | if not safereq then 284 | _safewait = true 285 | break 286 | end 287 | end 288 | end 289 | else 290 | // Oops, the connection is closed. Open it and try sending 291 | // again when it becomes active. 292 | _new_conn() 293 | end 294 | 295 | fun ref _new_conn() => 296 | """ 297 | Creates a new connection. 298 | """ 299 | match _conn 300 | | let _: None => 301 | try 302 | let ctx = _sslctx as SSLContext 303 | let ssl = ctx.client(_host)? 304 | TCPConnection( 305 | _auth, 306 | SSLConnection(_ClientConnHandler(this, _keepalive_timeout_secs), consume ssl), 307 | _host, _service) 308 | else 309 | TCPConnection( 310 | _auth, 311 | _ClientConnHandler(this, _keepalive_timeout_secs), 312 | _host, _service) 313 | end 314 | _conn = _ConnConnecting 315 | end 316 | 317 | fun ref _cancel_all(): Bool => 318 | """ 319 | Cancel all pending requests. 320 | 321 | Returns true if any requests have been cancelled. 322 | """ 323 | var cancelled = false 324 | try 325 | while true do 326 | _unsent.pop()? 327 | cancelled = true 328 | end 329 | end 330 | 331 | for node in _sent.nodes() do 332 | node.remove() 333 | try 334 | node.pop()? 335 | end 336 | cancelled = true 337 | end 338 | cancelled 339 | 340 | be _mute() => 341 | """ 342 | The application can not handle any more data for a while. 343 | """ 344 | try (_conn as TCPConnection).mute() end 345 | 346 | be _unmute() => 347 | """ 348 | The application can accept more data. 349 | """ 350 | try (_conn as TCPConnection).unmute() end 351 | -------------------------------------------------------------------------------- /http/_host_service.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | 3 | class val _HostService is (Hashable & Equatable[_HostService]) 4 | let scheme: String 5 | let host: String 6 | let service: String 7 | 8 | new val create(scheme': String, host': String, service': String) => 9 | scheme = scheme' 10 | host = host' 11 | service = service' 12 | 13 | fun hash(): USize => 14 | scheme.hash() xor host.hash() xor service.hash() 15 | 16 | fun eq(that: _HostService box): Bool => 17 | (scheme == that.scheme) 18 | and (host == that.host) 19 | and (service == that.service) 20 | -------------------------------------------------------------------------------- /http/_test.pony: -------------------------------------------------------------------------------- 1 | use "pony_test" 2 | use "buffered" 3 | use "collections" 4 | use "net" 5 | use "time" 6 | 7 | actor \nodoc\ Main is TestList 8 | new create(env: Env) => PonyTest(env, this) 9 | new make() => None 10 | 11 | fun tag tests(test: PonyTest) => 12 | _ClientErrorHandlingTests.make().tests(test) 13 | _ClientTests.make().tests(test) 14 | 15 | test(_Encode) 16 | test(_EncodeBad) 17 | test(_EncodeIPv6) 18 | test(_EncodeClean) 19 | 20 | test(_Check) 21 | test(_CheckBad) 22 | test(_CheckScheme) 23 | test(_CheckIPv6) 24 | 25 | test(_Decode) 26 | test(_DecodeBad) 27 | 28 | test(_BuildBasic) 29 | test(_BuildMissingParts) 30 | test(_BuildBad) 31 | test(_BuildNoEncoding) 32 | test(_Valid) 33 | test(_ToStringFun) 34 | 35 | test(_HTTPConnTest) 36 | test(_HTTPParserNoBodyTest) 37 | test(_HTTPParserOneshotBodyTest) 38 | test(_HTTPParserStreamedBodyTest) 39 | 40 | test(_PayloadHeadersAreCaseInsensitive) 41 | 42 | class \nodoc\ iso _Encode is UnitTest 43 | fun name(): String => "http/URLEncode.encode" 44 | 45 | fun apply(h: TestHelper) ? => 46 | // Unreserved chars, decoded. 47 | h.assert_eq[String]("Aa4-._~Aa4-._~", 48 | URLEncode.encode("Aa4-._~%41%61%34%2D%2E%5F%7E", URLPartUser)?) 49 | 50 | h.assert_eq[String]("F_12x", URLEncode.encode("F_1%32x", URLPartPassword)?) 51 | h.assert_eq[String]("F_12x", URLEncode.encode("F_1%32x", URLPartHost)?) 52 | h.assert_eq[String]("F_12x", URLEncode.encode("F_1%32x", URLPartPath)?) 53 | h.assert_eq[String]("F_12x", URLEncode.encode("F_1%32x", URLPartQuery)?) 54 | h.assert_eq[String]("F_12x", URLEncode.encode("F_1%32x", URLPartFragment)?) 55 | 56 | // Sub-delimiters, left encoded or not as original. 57 | h.assert_eq[String]("!$&'()*+,;=%21%24%26%27%28%29%2A%2B%2C%3B%3D", 58 | URLEncode.encode("!$&'()*+,;=%21%24%26%27%28%29%2A%2B%2C%3B%3D", 59 | URLPartUser)?) 60 | 61 | h.assert_eq[String](",%2C", URLEncode.encode(",%2C", URLPartPassword)?) 62 | h.assert_eq[String](",%2C", URLEncode.encode(",%2C", URLPartHost)?) 63 | h.assert_eq[String](",%2C", URLEncode.encode(",%2C", URLPartPath)?) 64 | h.assert_eq[String](",%2C", URLEncode.encode(",%2C", URLPartQuery)?) 65 | h.assert_eq[String](",%2C", URLEncode.encode(",%2C", URLPartFragment)?) 66 | 67 | // Misc characters, encoded. 68 | h.assert_eq[String]("%23%3C%3E%5B%5D%7B%7D%7C%5E%20" + 69 | "%23%3C%3E%5B%5D%7B%7D%7C%5E%25", 70 | URLEncode.encode("#<>[]{}|^ %23%3C%3E%5B%5D%7B%7D%7C%5E%25", 71 | URLPartUser)?) 72 | 73 | h.assert_eq[String]("%23%23", URLEncode.encode("#%23", URLPartPassword)?) 74 | h.assert_eq[String]("%23%23", URLEncode.encode("#%23", URLPartHost)?) 75 | h.assert_eq[String]("%23%23", URLEncode.encode("#%23", URLPartPath)?) 76 | h.assert_eq[String]("%23%23", URLEncode.encode("#%23", URLPartQuery)?) 77 | h.assert_eq[String]("%23%23", URLEncode.encode("#%23", URLPartFragment)?) 78 | 79 | // Delimiters, whether encoded depends on URL part. 80 | h.assert_eq[String]("%3A%40%2F%3F", URLEncode.encode(":@/?", URLPartUser)?) 81 | h.assert_eq[String](":%40%2F%3F", 82 | URLEncode.encode(":@/?", URLPartPassword)?) 83 | h.assert_eq[String]("%3A%40%2F%3F", URLEncode.encode(":@/?", URLPartHost)?) 84 | h.assert_eq[String](":@/%3F", URLEncode.encode(":@/?", URLPartPath)?) 85 | h.assert_eq[String](":@/?", URLEncode.encode(":@/?", URLPartQuery)?) 86 | h.assert_eq[String](":@/?", URLEncode.encode(":@/?", URLPartFragment)?) 87 | 88 | class \nodoc\ iso _EncodeBad is UnitTest 89 | fun name(): String => "http/URLEncode.encode_bad" 90 | 91 | fun apply(h: TestHelper) => 92 | h.assert_error({() ? => URLEncode.encode("%2G", URLPartUser)? }) 93 | h.assert_error({() ? => URLEncode.encode("%xx", URLPartUser)? }) 94 | h.assert_error({() ? => URLEncode.encode("%2", URLPartUser)? }) 95 | 96 | class \nodoc\ iso _EncodeIPv6 is UnitTest 97 | fun name(): String => "http/URLEncode.encode_ipv6" 98 | 99 | fun apply(h: TestHelper) ? => 100 | // Allowed hex digits, '.' and ':' only, between '[' and ']'. 101 | h.assert_eq[String]("[1::A.B]", URLEncode.encode("[1::A.B]", URLPartHost)?) 102 | h.assert_error({() ? => URLEncode.encode("[G]", URLPartHost)? }) 103 | h.assert_error({() ? => URLEncode.encode("[/]", URLPartHost)? }) 104 | h.assert_error({() ? => URLEncode.encode("[%32]", URLPartHost)? }) 105 | h.assert_error({() ? => URLEncode.encode("[1]2", URLPartHost)? }) 106 | h.assert_error({() ? => URLEncode.encode("[1", URLPartHost)? }) 107 | h.assert_eq[String]("1%5D", URLEncode.encode("1]", URLPartHost)?) 108 | 109 | class \nodoc\ iso _EncodeClean is UnitTest 110 | fun name(): String => "http/URLEncode.encode_clean" 111 | 112 | fun apply(h: TestHelper) ? => 113 | // No percent encoding in source string. 114 | h.assert_eq[String]("F_1x", URLEncode.encode("F_1x", URLPartQuery, false)?) 115 | h.assert_eq[String]("%2541", URLEncode.encode("%41", URLPartQuery, false)?) 116 | h.assert_eq[String]("%25", URLEncode.encode("%", URLPartQuery, false)?) 117 | 118 | class \nodoc\ iso _Check is UnitTest 119 | fun name(): String => "http/URLEncode.check" 120 | 121 | fun apply(h: TestHelper) => 122 | // Unreserved chars, legal encoded or not. 123 | h.assert_eq[Bool](true, 124 | URLEncode.check("Aa4-._~%41%61%34%2D%2E%5F%7E", URLPartUser)) 125 | 126 | h.assert_eq[Bool](true, URLEncode.check("F_1%32x", URLPartPassword)) 127 | h.assert_eq[Bool](true, URLEncode.check("F_1%32x", URLPartHost)) 128 | h.assert_eq[Bool](true, URLEncode.check("F_1%32x", URLPartPath)) 129 | h.assert_eq[Bool](true, URLEncode.check("F_1%32x", URLPartQuery)) 130 | h.assert_eq[Bool](true, URLEncode.check("F_1%32x", URLPartFragment)) 131 | 132 | // Sub-delimiters, legal encoded or not. 133 | h.assert_eq[Bool](true, 134 | URLEncode.check("!$&'()*+,;=%21%24%26%27%28%29%2A%2B%2C%3B%3D", 135 | URLPartUser)) 136 | 137 | h.assert_eq[Bool](true, URLEncode.check(",%2C", URLPartPassword)) 138 | h.assert_eq[Bool](true, URLEncode.check(",%2C", URLPartHost)) 139 | h.assert_eq[Bool](true, URLEncode.check(",%2C", URLPartPath)) 140 | h.assert_eq[Bool](true, URLEncode.check(",%2C", URLPartQuery)) 141 | h.assert_eq[Bool](true, URLEncode.check(",%2C", URLPartFragment)) 142 | 143 | // Misc characters, must be encoded. 144 | h.assert_eq[Bool](true, 145 | URLEncode.check("%23%3C%3E%5B%5D%7B%7D%7C%5E%25", URLPartUser)) 146 | h.assert_eq[Bool](false, URLEncode.check("<", URLPartUser)) 147 | h.assert_eq[Bool](false, URLEncode.check(">", URLPartUser)) 148 | h.assert_eq[Bool](false, URLEncode.check("|", URLPartUser)) 149 | h.assert_eq[Bool](false, URLEncode.check("^", URLPartUser)) 150 | 151 | h.assert_eq[Bool](true, URLEncode.check("%23%3C", URLPartPassword)) 152 | h.assert_eq[Bool](false, URLEncode.check("<", URLPartPassword)) 153 | h.assert_eq[Bool](true, URLEncode.check("%23%3C", URLPartHost)) 154 | h.assert_eq[Bool](false, URLEncode.check("<", URLPartHost)) 155 | h.assert_eq[Bool](true, URLEncode.check("%23%3C", URLPartPath)) 156 | h.assert_eq[Bool](false, URLEncode.check("<", URLPartPath)) 157 | h.assert_eq[Bool](true, URLEncode.check("%23%3C", URLPartQuery)) 158 | h.assert_eq[Bool](false, URLEncode.check("<", URLPartQuery)) 159 | h.assert_eq[Bool](true, URLEncode.check("%23%3C", URLPartFragment)) 160 | h.assert_eq[Bool](false, URLEncode.check("<", URLPartFragment)) 161 | 162 | // Delimiters, whether need to be encoded depends on URL part. 163 | h.assert_eq[Bool](true, URLEncode.check("%3A%40%2F%3F", URLPartUser)) 164 | h.assert_eq[Bool](false, URLEncode.check(":", URLPartUser)) 165 | h.assert_eq[Bool](false, URLEncode.check("@", URLPartUser)) 166 | h.assert_eq[Bool](false, URLEncode.check("/", URLPartUser)) 167 | h.assert_eq[Bool](false, URLEncode.check("?", URLPartUser)) 168 | h.assert_eq[Bool](true, URLEncode.check(":%40%2F%3F", URLPartPassword)) 169 | h.assert_eq[Bool](false, URLEncode.check("@", URLPartPassword)) 170 | h.assert_eq[Bool](false, URLEncode.check("/", URLPartPassword)) 171 | h.assert_eq[Bool](false, URLEncode.check("?", URLPartPassword)) 172 | h.assert_eq[Bool](true, URLEncode.check("%3A%40%2F%3F", URLPartHost)) 173 | h.assert_eq[Bool](false, URLEncode.check(":", URLPartHost)) 174 | h.assert_eq[Bool](false, URLEncode.check("@", URLPartHost)) 175 | h.assert_eq[Bool](false, URLEncode.check("/", URLPartHost)) 176 | h.assert_eq[Bool](false, URLEncode.check("?", URLPartHost)) 177 | h.assert_eq[Bool](true, URLEncode.check(":@/%3F", URLPartPath)) 178 | h.assert_eq[Bool](false, URLEncode.check("?", URLPartPath)) 179 | h.assert_eq[Bool](true, URLEncode.check(":@/?", URLPartQuery)) 180 | h.assert_eq[Bool](true, URLEncode.check(":@/?", URLPartFragment)) 181 | 182 | class \nodoc\ iso _CheckBad is UnitTest 183 | fun name(): String => "http/URLEncode.check_bad" 184 | 185 | fun apply(h: TestHelper) => 186 | h.assert_eq[Bool](false, URLEncode.check("%2G", URLPartUser)) 187 | h.assert_eq[Bool](false, URLEncode.check("%xx", URLPartUser)) 188 | h.assert_eq[Bool](false, URLEncode.check("%2", URLPartUser)) 189 | 190 | class \nodoc\ iso _CheckScheme is UnitTest 191 | fun name(): String => "http/URLEncode.check_scheme" 192 | 193 | fun apply(h: TestHelper) => 194 | h.assert_eq[Bool](true, URLEncode.check_scheme("Aa4-+.")) 195 | h.assert_eq[Bool](false, URLEncode.check_scheme("_")) 196 | h.assert_eq[Bool](false, URLEncode.check_scheme(":")) 197 | h.assert_eq[Bool](false, URLEncode.check_scheme("%41")) 198 | 199 | class \nodoc\ iso _CheckIPv6 is UnitTest 200 | fun name(): String => "http/URLEncode.check_ipv6" 201 | 202 | fun apply(h: TestHelper) => 203 | // Allowed hex digits, '.' and ':' only, between '[' and ']'. 204 | h.assert_eq[Bool](true, URLEncode.check("[1::A.B]", URLPartHost)) 205 | h.assert_eq[Bool](false, URLEncode.check("[G]", URLPartHost)) 206 | h.assert_eq[Bool](false, URLEncode.check("[/]", URLPartHost)) 207 | h.assert_eq[Bool](false, URLEncode.check("[%32]", URLPartHost)) 208 | h.assert_eq[Bool](false, URLEncode.check("[1]2", URLPartHost)) 209 | h.assert_eq[Bool](false, URLEncode.check("[1", URLPartHost)) 210 | h.assert_eq[Bool](false, URLEncode.check("1]", URLPartHost)) 211 | 212 | class \nodoc\ iso _Decode is UnitTest 213 | fun name(): String => "http/URLEncode.decode" 214 | 215 | fun apply(h: TestHelper) ? => 216 | h.assert_eq[String]("Aa4-._~Aa4-._~", 217 | URLEncode.decode("Aa4-._~%41%61%34%2D%2E%5F%7E")?) 218 | 219 | h.assert_eq[String]("F_12x", URLEncode.decode("F_1%32x")?) 220 | 221 | h.assert_eq[String]("!$&'()* ,;=!$&'()*+,;=", 222 | URLEncode.decode("!$&'()*+,;=%21%24%26%27%28%29%2A%2B%2C%3B%3D")?) 223 | 224 | h.assert_eq[String]("#<>[]{}|^ #<>[]{}|^ %", 225 | URLEncode.decode("#<>[]{}|^ %23%3C%3E%5B%5D%7B%7D%7C%5E%20%25")?) 226 | 227 | class \nodoc\ iso _DecodeBad is UnitTest 228 | fun name(): String => "http/URLEncode.decode_bad" 229 | 230 | fun apply(h: TestHelper) => 231 | h.assert_error({() ? => URLEncode.decode("%2G")? }) 232 | h.assert_error({() ? => URLEncode.decode("%xx")? }) 233 | h.assert_error({() ? => URLEncode.decode("%2")? }) 234 | 235 | class \nodoc\ iso _BuildBasic is UnitTest 236 | fun name(): String => "http/URL.build_basic" 237 | 238 | fun apply(h: TestHelper) ? => 239 | _Test(h, 240 | URL.build("https://user:password@host.name:12345/path?query#fragment")?, 241 | "https", "user", "password", "host.name", 12345, "/path", "query", 242 | "fragment") 243 | 244 | _Test(h, 245 | URL.build("http://rosettacode.org/wiki/Category]Pony")?, 246 | "http", "", "", "rosettacode.org", 80, "/wiki/Category%5DPony", "", "") 247 | 248 | _Test(h, 249 | URL.build("https://en.wikipedia.org/wiki/Polymorphism_" + 250 | "(computer_science)#Parametric_polymorphism")?, 251 | "https", "", "", "en.wikipedia.org", 443, 252 | "/wiki/Polymorphism_(computer_science)", "", 253 | "Parametric_polymorphism") 254 | 255 | _Test(h, URL.build("http://user@host")?, 256 | "http", "user", "", "host", 80, "/", "", "") 257 | 258 | class \nodoc\ iso _BuildMissingParts is UnitTest 259 | fun name(): String => "http/URL.build_missing_parts" 260 | 261 | fun apply(h: TestHelper) ? => 262 | _Test(h, URL.build("https://user@host.name/path#fragment")?, 263 | "https", "user", "", "host.name", 443, "/path", "", "fragment") 264 | 265 | _Test(h, URL.build("https://user@host.name#fragment")?, 266 | "https", "user", "", "host.name", 443, "/", "", "fragment") 267 | 268 | _Test(h, URL.build("//host.name/path")?, 269 | "", "", "", "host.name", 0, "/path", "", "") 270 | 271 | _Test(h, URL.build("/path")?, 272 | "", "", "", "", 0, "/path", "", "") 273 | 274 | _Test(h, URL.build("?query")?, 275 | "", "", "", "", 0, "/", "query", "") 276 | 277 | _Test(h, URL.build("#fragment")?, 278 | "", "", "", "", 0, "/", "", "fragment") 279 | 280 | _Test(h, URL.build("https://host.name/path#frag?ment")?, 281 | "https", "", "", "host.name", 443, "/path", "", "frag?ment") 282 | 283 | _Test(h, URL.build("https://user@host.name?quer/y#fragment")?, 284 | "https", "user", "", "host.name", 443, "/", "quer/y", "fragment") 285 | 286 | class \nodoc\ iso _BuildBad is UnitTest 287 | fun name(): String => "http/URL.build_bad" 288 | 289 | fun apply(h: TestHelper) => 290 | h.assert_error({() ? => 291 | URL.build("htt_ps://user@host.name/path#fragment")? 292 | }) 293 | 294 | h.assert_error({() ? => 295 | URL.build("https://[11::24_]/path")? 296 | }) 297 | 298 | h.assert_error({() ? => 299 | URL.build("https://[11::24/path")? 300 | }) 301 | 302 | h.assert_error({() ? => 303 | URL.build("https://host%2Gname/path")? 304 | }) 305 | 306 | h.assert_error({() ? => 307 | URL.build("https://hostname/path%")? 308 | }) 309 | 310 | class \nodoc\ iso _BuildNoEncoding is UnitTest 311 | fun name(): String => "http/URL.build_no_encoding" 312 | 313 | fun apply(h: TestHelper) ? => 314 | _Test(h, URL.build("https://host.name/path%32path", false)?, 315 | "https", "", "", "host.name", 443, "/path%2532path", "", "") 316 | 317 | class \nodoc\ iso _Valid is UnitTest 318 | fun name(): String => "http/URL.valid" 319 | 320 | fun apply(h: TestHelper) ? => 321 | _Test(h, 322 | URL.valid("https://user:password@host.name:12345/path?query#fragment")?, 323 | "https", "user", "password", "host.name", 12345, "/path", "query", 324 | "fragment") 325 | 326 | h.assert_error({() ? => 327 | URL.valid("http://rosettacode.org/wiki/Category[Pony]")? 328 | }) 329 | 330 | h.assert_error({() ? => 331 | URL.valid("https://en.wikipedia|org/wiki/Polymorphism_" + 332 | "(computer_science)#Parametric_polymorphism")? 333 | }) 334 | 335 | _Test(h, URL.valid("http://user@host")?, 336 | "http", "user", "", "host", 80, "/", "", "") 337 | 338 | class \nodoc\ iso _ToStringFun is UnitTest 339 | fun name(): String => "http/URL.to_string" 340 | 341 | fun apply(h: TestHelper) ? => 342 | h.assert_eq[String]( 343 | "https://user:password@host.name:12345/path?query#fragment", 344 | URL.build("https://user:password@host.name:12345/path?query#fragment")? 345 | .string()) 346 | 347 | h.assert_eq[String]("http://rosettacode.org/wiki/Category%5DPony", 348 | URL.build("http://rosettacode.org/wiki/Category]Pony")?.string()) 349 | 350 | h.assert_eq[String]("http://user@host/", 351 | URL.build("http://user@host")?.string()) 352 | 353 | // Default ports should be omitted. 354 | h.assert_eq[String]("http://host.name/path", 355 | URL.build("http://host.name:80/path")?.string()) 356 | 357 | primitive \nodoc\ _Test 358 | fun apply( 359 | h: TestHelper, 360 | url: URL, 361 | scheme: String, 362 | user: String, 363 | password: String, 364 | host: String, 365 | port: U16, 366 | path: String, 367 | query: String, 368 | fragment: String) 369 | => 370 | h.assert_eq[String](scheme, url.scheme) 371 | h.assert_eq[String](user, url.user) 372 | h.assert_eq[String](password, url.password) 373 | h.assert_eq[String](host, url.host) 374 | h.assert_eq[U16](port, url.port) 375 | h.assert_eq[String](path, url.path) 376 | h.assert_eq[String](query, url.query) 377 | h.assert_eq[String](fragment, url.fragment) 378 | 379 | // Actor and classes to test the HTTPClient and modified _HTTPConnection. 380 | class \nodoc\ _HTTPConnTestHandler is HTTPHandler 381 | var n_received: U32 = 0 382 | let h: TestHelper 383 | 384 | new create(h': TestHelper) => 385 | h = h' 386 | h.complete_action("client handler create called") 387 | 388 | fun ref apply(payload: Payload val): Any => 389 | n_received = n_received + 1 390 | h.complete_action("client handler apply called " + n_received.string()) 391 | 392 | fun ref chunk(data: ByteSeq val) => 393 | h.log("_HTTPConnTestHandler.chunk called") 394 | 395 | class \nodoc\ val _HTTPConnTestHandlerFactory is HandlerFactory 396 | let h: TestHelper 397 | 398 | new val create(h': TestHelper) => 399 | h = h' 400 | 401 | fun apply(session: HTTPSession): HTTPHandler ref^ => 402 | h.dispose_when_done(session) 403 | h.complete_action("client factory apply called") 404 | _HTTPConnTestHandler(h) 405 | 406 | class \nodoc\ iso _HTTPConnTest is UnitTest 407 | fun name(): String => "http/_HTTPConnection._new_conn" 408 | fun label(): String => "conn-fix" 409 | 410 | fun ref apply(h: TestHelper) => 411 | // Set expectations. 412 | h.expect_action("client factory apply called") 413 | h.expect_action("client handler create called") 414 | h.expect_action("client handler apply called 1") 415 | h.expect_action("client handler apply called 2") 416 | h.expect_action("server writing reponse 1") 417 | h.expect_action("server writing reponse 2") 418 | h.expect_action("server listening") 419 | h.expect_action("server listen connected") 420 | h.expect_action("server connection accepted") 421 | h.expect_action("server connection closed") 422 | 423 | let worker = object 424 | var client: (HTTPClient iso | None) = None 425 | 426 | be listening(service: String) => 427 | try 428 | // Need two or more request to check if the fix works. 429 | let loops: USize = 2 430 | // let service: String val = "12345" 431 | h.log("received service: [" + service + "]") 432 | let us: String = "http://localhost:" + service 433 | h.log("URL: " + us) 434 | let url = URL.build(us)? 435 | h.log("url.string()=" + url.string()) 436 | let hf = _HTTPConnTestHandlerFactory(h) 437 | client = recover iso HTTPClient(TCPConnectAuth(h.env.root), hf) end 438 | 439 | for _ in Range(0, loops) do 440 | let payload: Payload iso = Payload.request("GET", url) 441 | payload.set_length(0) 442 | try 443 | (client as HTTPClient iso)(consume payload)? 444 | end 445 | end 446 | else 447 | h.log("Error in worker.listening") 448 | h.complete(false) 449 | end // try 450 | end // object 451 | 452 | // Start the fake server. 453 | h.dispose_when_done( 454 | TCPListener.ip4( 455 | TCPListenAuth(h.env.root), 456 | _FixedResponseHTTPServerNotify( 457 | h, 458 | {(p: String val) => 459 | worker.listening(p) 460 | None 461 | }, 462 | recover 463 | [ as String val: 464 | "HTTP/1.1 200 OK" 465 | "Server: pony_fake_server" 466 | "Content-Length: 0" 467 | "Status: 200 OK" 468 | "" 469 | ] 470 | end 471 | ), 472 | "", // all interfaces 473 | "0" // random service 474 | ) 475 | ) 476 | 477 | // Start a long test for 5 seconds. 478 | h.long_test(5_000_000_000) 479 | 480 | primitive \nodoc\ _FixedResponseHTTPServerNotify 481 | """ 482 | Test http server that spits out fixed responses. 483 | apply returns a TCPListenNotify object. 484 | """ 485 | 486 | fun apply( 487 | h': TestHelper, 488 | f: {(String val)} iso, 489 | r: Array[String val] val) 490 | : TCPListenNotify iso^ 491 | => 492 | recover 493 | object iso is TCPListenNotify 494 | let h: TestHelper = h' 495 | let listen_cb: {(String val)} iso = consume f 496 | let response: Array[String val] val = r 497 | 498 | fun ref listening(listen: TCPListener ref) => 499 | try 500 | // Get the service as numeric. 501 | let name = listen.local_address().name()? 502 | h.log("listening on: " + name._1 + ":" + name._2) 503 | listen_cb(name._2) 504 | h.dispose_when_done(listen) 505 | h.complete_action("server listening") 506 | end 507 | 508 | fun ref not_listening(listen: TCPListener ref) => 509 | h.fail_action("server listening") 510 | h.log("not_listening") 511 | 512 | fun ref closed(listen: TCPListener ref) => 513 | h.log("closed") 514 | 515 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ => 516 | h.complete_action("server listen connected") 517 | recover 518 | object iso is TCPConnectionNotify 519 | // let response': Array[String val] val = response 520 | let reader: Reader iso = Reader 521 | var nr: USize = 0 522 | 523 | fun ref received( 524 | conn: TCPConnection ref, 525 | data: Array[U8] iso, 526 | times: USize) 527 | : Bool 528 | => 529 | reader.append(consume data) 530 | while true do 531 | var blank = false 532 | try 533 | let l = reader.line()? 534 | let l_size = l.size() 535 | h.log("received line: " + consume l) 536 | if l_size == 0 then 537 | // Write the response. 538 | nr = nr + 1 539 | for r in response.values() do 540 | h.log("[" + r + "]") 541 | conn.write(r + "\r\n") 542 | end 543 | h.complete_action( 544 | "server writing reponse " + nr.string()) 545 | end 546 | else 547 | h.log("breaking") 548 | break 549 | end 550 | 551 | end // while 552 | true 553 | 554 | fun ref accepted(conn: TCPConnection ref) => 555 | h.complete_action("server connection accepted") 556 | h.dispose_when_done(conn) 557 | 558 | fun ref closed(conn: TCPConnection ref) => 559 | h.complete_action("server connection closed") 560 | 561 | fun ref connecting(conn: TCPConnection ref, count: U32) => 562 | h.log("connecting") 563 | None 564 | 565 | fun ref connect_failed(conn: TCPConnection ref) => 566 | h.log("connect_failed") 567 | None 568 | 569 | fun ref throttled(conn: TCPConnection ref) => 570 | h.log("throttled") 571 | 572 | fun ref unthrottled(conn: TCPConnection ref) => 573 | h.log("unthrottled") 574 | end // object 575 | end // recover 576 | 577 | end // object 578 | end // recover 579 | 580 | class \nodoc\ iso _HTTPParserNoBodyTest is UnitTest 581 | fun name(): String => "http/HTTPParser.NoBody" 582 | fun ref apply(h: TestHelper) => 583 | let test_session = 584 | object is HTTPSession 585 | be apply(payload: Payload val) => None 586 | be finish() => None 587 | be dispose() => None 588 | be write(byteseq: ByteSeq val) => None 589 | be _mute() => None 590 | be _unmute() => None 591 | be cancel(msg: Payload val) => None 592 | be _deliver(payload: Payload val) => 593 | h.complete_action("_deliver") 594 | try 595 | h.assert_eq[USize](payload.body()?.size(), 0) 596 | else 597 | h.fail("failed to get empty oneshot body.") 598 | end 599 | 600 | be _chunk(data: ByteSeq val) => 601 | h.fail("HTTPSession._chunk called.") 602 | be _finish() => 603 | h.fail("HTTPSession._finish called.") 604 | end 605 | let parser = HTTPParser.request(test_session) 606 | let payload: String = "\r\n".join([ 607 | "GET /get HTTP/1.1" 608 | "Host: httpbin.org" 609 | "User-Agent: curl/7.58.0" 610 | "Accept: */*" 611 | "" 612 | "" 613 | ].values()) 614 | 615 | h.long_test(2_000_000_000) 616 | h.expect_action("_deliver") 617 | let reader: Reader = Reader 618 | reader.append(payload) 619 | match parser.parse(reader) 620 | | ParseError => h.fail("parser failed to parse request") 621 | end 622 | 623 | class \nodoc\ iso _HTTPParserOneshotBodyTest is UnitTest 624 | fun name(): String => "http/HTTPParser.OneshotBody" 625 | fun ref apply(h: TestHelper) => 626 | let body = "custname=Pony+Mc+Ponyface&custtel=%2B490123456789&custemail=pony%40ponylang.org&size=large&topping=bacon&topping=cheese&topping=onion&delivery=&comments=This+is+a+stupid+test" 627 | let test_session = 628 | object is HTTPSession 629 | be apply(payload: Payload val) => None 630 | be finish() => None 631 | be dispose() => None 632 | be write(byteseq: ByteSeq val) => None 633 | be _mute() => None 634 | be _unmute() => None 635 | be cancel(msg: Payload val) => None 636 | be _deliver(payload: Payload val) => 637 | h.complete_action("_deliver") 638 | try 639 | let received_body: String = 640 | recover val 641 | let tmp = payload.body()? 642 | let buf = String(body.size()) 643 | for chunk in tmp.values() do 644 | buf.append(chunk) 645 | end 646 | buf 647 | end 648 | h.assert_eq[String](received_body, body) 649 | else 650 | h.fail("failed to get oneshot body.") 651 | end 652 | be _chunk(data: ByteSeq val) => 653 | h.fail("HTTPSession._chunk called.") 654 | be _finish() => 655 | h.fail("HTTPSession._finish called.") 656 | end 657 | let parser = HTTPParser.request(test_session) 658 | let payload: String = "\r\n".join([ 659 | "POST /post HTTP/1.1" 660 | "Host: httpbin.org" 661 | "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0" 662 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 663 | "Accept-Language: en-GB,en;q=0.5" 664 | "Accept-Encoding: gzip, deflate" 665 | "Referer: http://httpbin.org/forms/post" 666 | "Content-Type: application/x-www-form-urlencoded" 667 | "Content-Length: 174" 668 | "Cookie: _gauges_unique_hour=1; _gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1" 669 | "Connection: keep-alive" 670 | "Upgrade-Insecure-Requests: 1" 671 | "" 672 | body 673 | ].values()) 674 | h.long_test(2_000_000_000) 675 | h.expect_action("_deliver") 676 | let reader: Reader = Reader 677 | reader.append(payload) 678 | match parser.parse(reader) 679 | | ParseError => h.fail("parser failed to parse request.") 680 | end 681 | 682 | class \nodoc\ iso _HTTPParserStreamedBodyTest is UnitTest 683 | fun name(): String => "http/HTTPParser.StreamedBody" 684 | fun apply(h: TestHelper) => 685 | let test_session = 686 | object is HTTPSession 687 | be apply(payload: Payload val) => None 688 | be finish() => None 689 | be dispose() => None 690 | be write(byteseq: ByteSeq val) => None 691 | be _mute() => None 692 | be _unmute() => None 693 | be cancel(msg: Payload val) => None 694 | be _deliver(payload: Payload val) => 695 | h.complete_action("_deliver") 696 | be _chunk(data: ByteSeq val) => 697 | h.complete_action("session._chunk") 698 | be _finish() => 699 | h.complete_action("session._finish") 700 | end 701 | let parser = HTTPParser.response(test_session) 702 | let payload: String = "\r\n".join([ 703 | "HTTP/1.1 200 OK" 704 | "Content-Length: 10001" 705 | "Content-Type: application/octet-stream" 706 | "" 707 | String.from_array(recover val Array[U8].init('a', 10001) end) 708 | ].values()) 709 | h.long_test(2_000_000_000) 710 | h.expect_action("_deliver") 711 | h.expect_action("session._chunk") 712 | h.expect_action("session._finish") 713 | let reader: Reader = Reader 714 | reader.append(payload) 715 | match parser.parse(reader) 716 | | ParseError => h.fail("parser failed to parse request.") 717 | end 718 | 719 | class \nodoc\ _PayloadHeadersAreCaseInsensitive is UnitTest 720 | fun name(): String => "http/Payload.HeadersAreCaseInsensitive" 721 | fun apply(h: TestHelper) ? => 722 | let url = URL.valid("https://example.com")? 723 | let request = Payload.request(where url' = url) 724 | let some_caps_header: String = recover val "Accept" end 725 | let all_lower_header: String = some_caps_header.lower() 726 | let all_upper_header: String = some_caps_header.upper() 727 | let header_value = "text/plain" 728 | 729 | request(some_caps_header) = header_value 730 | 731 | h.assert_eq[String](header_value, request(all_lower_header)?) 732 | h.assert_eq[String](header_value, request(all_upper_header)?) 733 | -------------------------------------------------------------------------------- /http/_test_client.pony: -------------------------------------------------------------------------------- 1 | use "pony_test" 2 | use "net" 3 | 4 | actor \nodoc\ _ClientTests is TestList 5 | new make() => 6 | None 7 | 8 | fun tag tests(test: PonyTest) => 9 | test(_ClientStreamTransferTest) 10 | 11 | class \nodoc\ val _StreamTransferHandlerFactory is HandlerFactory 12 | let _h: TestHelper 13 | var expected_length: USize = 0 14 | var received_size: USize = 0 15 | new val create(h: TestHelper) => 16 | _h = h 17 | 18 | fun apply(session: HTTPSession): HTTPHandler ref^ => 19 | object is HTTPHandler 20 | fun ref apply(payload: Payload val): Any => 21 | _h.complete_action("receive response headers") 22 | expected_length = 23 | try 24 | payload("Content-Length")?.usize()? 25 | else 26 | _h.fail("failed to extract Content-Length") 27 | -1 28 | end 29 | fun ref chunk(data: ByteSeq val) => 30 | // ensure we receive at least one chunk 31 | _h.complete_action("chunk") 32 | received_size = received_size + data.size() 33 | fun ref finished() => 34 | _h.complete_action("finished") 35 | // ensure size equals 36 | _h.assert_eq[USize](expected_length, received_size) 37 | 38 | fun ref failed(reason: HTTPFailureReason) => 39 | _h.fail("failed") 40 | end 41 | 42 | class \nodoc\ iso _ClientStreamTransferTest is UnitTest 43 | fun name(): String => "client/stream-transfer" 44 | fun apply(h: TestHelper) => 45 | h.long_test(2_000_000_000) 46 | 47 | h.expect_action("server listening") 48 | h.expect_action("server connection accepted") 49 | h.expect_action("receive response headers") 50 | h.expect_action("chunk") 51 | h.expect_action("finished") 52 | 53 | let notify = object iso is TCPListenNotify 54 | let _h: TestHelper = h 55 | 56 | fun ref listening(listen: TCPListener ref) => 57 | _h.complete_action("server listening") 58 | try 59 | let client = HTTPClient( 60 | TCPConnectAuth(_h.env.root), 61 | _StreamTransferHandlerFactory(_h), 62 | None 63 | where keepalive_timeout_secs = U32(2) 64 | ) 65 | (let host, let port) = listen.local_address().name()? 66 | _h.log("connecting to server at " + host + ":" + port) 67 | let req = Payload.request("GET", URL.build("http://" + host + ":" + port + "/bla")?) 68 | client(consume req)? 69 | else 70 | _h.fail("request building failed") 71 | end 72 | 73 | fun ref not_listening(listen: TCPListener ref) => 74 | _h.fail_action("server listening") 75 | _h.log("not_listening") 76 | 77 | fun ref closed(listen: TCPListener ref) => 78 | _h.log("TCP listener closed") 79 | 80 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ => 81 | object iso is TCPConnectionNotify 82 | var written: Bool = false 83 | fun ref received(conn: TCPConnection ref, data: Array[U8] iso, times: USize): Bool => 84 | _h.log("received stuff") 85 | if not written then 86 | conn.write("\r\n".join([ 87 | "HTTP/1.1 200 OK" 88 | "Server: Bla" 89 | "Content-Length: 10004" 90 | "Content-Type: application/octet-stream" 91 | "" 92 | "" 93 | ].values())) 94 | conn.write(recover val Array[U8].init('a', 2501) end) 95 | conn.write(recover val Array[U8].init('b', 2501) end) 96 | conn.write(recover val Array[U8].init('c', 2501) end) 97 | conn.write(recover val Array[U8].init('d', 2501) end) 98 | written = true 99 | end 100 | true 101 | 102 | fun ref accepted(conn: TCPConnection ref) => 103 | _h.complete_action("server connection accepted") 104 | _h.dispose_when_done(conn) 105 | 106 | fun ref connect_failed(conn: TCPConnection ref) => 107 | _h.fail("connection failed") 108 | 109 | fun ref closed(conn: TCPConnection ref) => 110 | _h.complete_action("server connection closed") 111 | end 112 | end 113 | 114 | let host = "127.0.0.1" 115 | let service = "0" 116 | 117 | let listener = TCPListener.ip4(TCPListenAuth(h.env.root), consume notify, host, service) 118 | h.dispose_when_done(listener) 119 | -------------------------------------------------------------------------------- /http/_test_client_error_handling.pony: -------------------------------------------------------------------------------- 1 | use "pony_test" 2 | 3 | use "net" 4 | use "net_ssl" 5 | use "files" 6 | 7 | actor \nodoc\ _ClientErrorHandlingTests is TestList 8 | new make() => 9 | None 10 | 11 | fun tag tests(test: PonyTest) => 12 | test(_ConnectionClosedTest) 13 | ifdef not windows then 14 | // Server sockets on Windows can be connected to long after they are dead 15 | test(_ConnectFailedTest) 16 | end 17 | test(_SSLAuthFailedTest) 18 | 19 | class \nodoc\ val _ConnectionClosedHandlerFactory is HandlerFactory 20 | let _h: TestHelper 21 | new val create(h: TestHelper) => 22 | _h = h 23 | 24 | fun apply(session: HTTPSession): HTTPHandler ref^ => 25 | object is HTTPHandler 26 | fun ref failed(reason: HTTPFailureReason) => 27 | match reason 28 | | ConnectionClosed => 29 | _h.complete_action("client failed with ConnectionClosed") 30 | else 31 | _h.fail_action("failed with sth else") 32 | end 33 | end 34 | 35 | class \nodoc\ iso _ConnectionClosedTest is UnitTest 36 | fun name(): String => "client/error-handling/connection-closed" 37 | 38 | fun apply(h: TestHelper) => 39 | h.long_test(5_000_000_000) 40 | 41 | h.expect_action("server listening") 42 | h.expect_action("server listen connected") 43 | h.expect_action("server connection accepted") 44 | h.expect_action("server connection closed") 45 | h.expect_action("client failed with ConnectionClosed") 46 | 47 | let notify = object iso is TCPListenNotify 48 | let _h: TestHelper = h 49 | fun ref listening(listen: TCPListener ref) => 50 | _h.complete_action("server listening") 51 | _h.log("listening") 52 | 53 | try 54 | let client = HTTPClient( 55 | TCPConnectAuth(_h.env.root), 56 | _ConnectionClosedHandlerFactory(_h), 57 | None 58 | where keepalive_timeout_secs = U32(2) 59 | ) 60 | (let host, let port) = listen.local_address().name()? 61 | let req = Payload.request("GET", 62 | URL.build("http://" + host + ":" + port + "/bla")?) 63 | req.add_chunk("CHUNK") 64 | client(consume req)? 65 | else 66 | _h.fail("request building failed") 67 | end 68 | 69 | fun ref not_listening(listen: TCPListener ref) => 70 | _h.fail_action("server listening") 71 | _h.log("not_listening") 72 | 73 | fun ref closed(listen: TCPListener ref) => 74 | _h.log("TCP listener closed") 75 | 76 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ => 77 | _h.complete_action("server listen connected") 78 | // server code 79 | object iso is TCPConnectionNotify 80 | fun ref received(conn: TCPConnection ref, data: Array[U8] iso, 81 | times: USize): Bool 82 | => 83 | conn.close() // trigger the error condition 84 | true 85 | 86 | fun ref accepted(conn: TCPConnection ref) => 87 | _h.complete_action("server connection accepted") 88 | _h.dispose_when_done(conn) 89 | 90 | fun ref connect_failed(conn: TCPConnection ref) => 91 | _h.fail("connection failed") 92 | 93 | fun ref closed(conn: TCPConnection ref) => 94 | _h.complete_action("server connection closed") 95 | end 96 | end 97 | 98 | let host = "127.0.0.1" 99 | let service = "0" 100 | 101 | let listener = TCPListener.ip4(TCPListenAuth(h.env.root), consume notify, 102 | host, service) 103 | h.dispose_when_done(listener) 104 | 105 | actor \nodoc\ _Connecter 106 | let _h: TestHelper 107 | 108 | new create(h: TestHelper) => 109 | _h = h 110 | 111 | be connect(host: String, port: String) => 112 | let port' = 113 | ifdef windows then 114 | port 115 | else 116 | port 117 | end 118 | 119 | _h.log("connecting to " + host + ":" + port') 120 | _h.complete_action("client trying to connect") 121 | try 122 | let client = HTTPClient( 123 | TCPConnectAuth(_h.env.root), 124 | _ConnectFailedHandlerFactory(_h), 125 | None 126 | where keepalive_timeout_secs = U32(2) 127 | ) 128 | let req = Payload.request("GET", 129 | URL.build("http://" + host + ":" + port' + "/bla")?) 130 | req.add_chunk("CHUNK") 131 | client(consume req)? 132 | else 133 | _h.fail("request building failed") 134 | end 135 | 136 | class \nodoc\ val _ConnectFailedHandlerFactory is HandlerFactory 137 | let _h: TestHelper 138 | 139 | new val create(h: TestHelper) => 140 | _h = h 141 | 142 | fun apply(session: HTTPSession): HTTPHandler ref^ => 143 | object is HTTPHandler 144 | fun ref finished() => 145 | _h.fail("failed by finishing") 146 | _h.complete(false) 147 | fun ref failed(reason: HTTPFailureReason) => 148 | match reason 149 | | AuthFailed => 150 | _h.fail("failed with AuthFailed") 151 | _h.complete(false) 152 | | ConnectionClosed => 153 | _h.fail("failed with ConnectionClosed") 154 | _h.complete(false) 155 | | ConnectFailed => 156 | _h.complete_action("client failed with ConnectFailed") 157 | end 158 | end 159 | 160 | class \nodoc\ iso _ConnectFailedTest is UnitTest 161 | fun name(): String => "client/error-handling/connect-failed" 162 | 163 | fun apply(h: TestHelper) => 164 | h.long_test(5_000_000_000) 165 | 166 | h.expect_action("server listening") 167 | h.expect_action("server closed") 168 | h.expect_action("client trying to connect") 169 | h.expect_action("client failed with ConnectFailed") 170 | 171 | let notify = object iso is TCPListenNotify 172 | let _h: TestHelper = h 173 | var host: String = "" 174 | var port: String = "" 175 | 176 | fun ref listening(listen: TCPListener ref) => 177 | _h.complete_action("server listening") 178 | try 179 | (host, port) = listen.local_address().name()? 180 | else 181 | _h.fail("unable to get port") 182 | end 183 | listen.close() 184 | 185 | fun ref not_listening(listen: TCPListener ref) => 186 | _h.fail_action("server listening") 187 | 188 | fun ref closed(listen: TCPListener ref) => 189 | _h.complete_action("server closed") 190 | let connecter = _Connecter(_h) 191 | connecter.connect(host, port) 192 | 193 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ => 194 | object iso is TCPConnectionNotify 195 | fun ref received(conn: TCPConnection ref, data: Array[U8] iso, 196 | times: USize): Bool => true 197 | fun ref accepted(conn: TCPConnection ref) => None 198 | fun ref connect_failed(conn: TCPConnection ref) => None 199 | fun ref closed(conn: TCPConnection ref) => None 200 | end 201 | end 202 | 203 | let host = "127.0.0.1" 204 | let service = "0" 205 | 206 | let listener = TCPListener.ip4(TCPListenAuth(h.env.root), consume notify, host, service) 207 | h.dispose_when_done(listener) 208 | 209 | 210 | primitive \nodoc\ _Paths 211 | fun join(paths: Array[String] box): String => 212 | var p = "" 213 | for path in paths.values() do 214 | p = Path.join(p, path) 215 | end 216 | p 217 | 218 | class \nodoc\ val _SSLAuthFailedHandlerFactory is HandlerFactory 219 | let _h: TestHelper 220 | new val create(h: TestHelper) => 221 | _h = h 222 | 223 | fun apply(session: HTTPSession): HTTPHandler ref^ => 224 | object is HTTPHandler 225 | fun ref failed(reason: HTTPFailureReason) => 226 | match reason 227 | | AuthFailed => 228 | _h.complete_action("client failed with AuthFailed") 229 | end 230 | end 231 | 232 | class \nodoc\ iso _SSLAuthFailedTest is UnitTest 233 | var cert_path: (FilePath | None) = None 234 | var key_path: (FilePath | None) = None 235 | var ca_path: (FilePath | None) = None 236 | 237 | fun name(): String => "client/error-handling/ssl-auth-failed" 238 | 239 | fun ref set_up(h: TestHelper) ? => 240 | // this shit is tightly coupled to 241 | // how the tests are called from the Makefile 242 | // which is not the nicest thing in the world 243 | let cwd = Path.cwd() 244 | cert_path = FilePath( 245 | FileAuth(h.env.root), 246 | _Paths.join([ 247 | cwd 248 | "http" 249 | "test" 250 | "cert.pem"]) 251 | ) 252 | if not (cert_path as FilePath).exists() then 253 | h.log("cert path: " + (cert_path as FilePath).path + " does not exist!") 254 | error 255 | end 256 | key_path = FilePath( 257 | FileAuth(h.env.root), 258 | _Paths.join([ 259 | cwd 260 | "http" 261 | "test" 262 | "key.pem"]) 263 | ) 264 | if not (key_path as FilePath).exists() then 265 | h.log("key path: " + (key_path as FilePath).path + " does not exist!") 266 | error 267 | end 268 | ifdef not windows then 269 | ca_path = FilePath(FileAuth(h.env.root), 270 | "/usr/share/ca-certificates/mozilla") 271 | if not (ca_path as FilePath).exists() then 272 | h.log("ca path: " + (ca_path as FilePath).path + " does not exist!") 273 | error 274 | end 275 | end 276 | 277 | fun apply(h: TestHelper) => 278 | h.long_test(5_000_000_000) 279 | 280 | h.expect_action("server listening") 281 | h.expect_action("client failed with AuthFailed") 282 | 283 | let notify = object iso is TCPListenNotify 284 | let _h: TestHelper = h 285 | var host: String = "" 286 | var port: String = "" 287 | 288 | fun ref listening(listen: TCPListener ref) => 289 | _h.complete_action("server listening") 290 | _h.log("listening") 291 | try 292 | (host, port) = listen.local_address().name()? 293 | try 294 | let ssl_ctx: SSLContext val = recover 295 | ifdef windows then 296 | SSLContext.>set_authority(None, None)? 297 | else 298 | SSLContext.>set_authority( 299 | None 300 | where path = ca_path as FilePath)? 301 | end 302 | end 303 | let client = HTTPClient( 304 | TCPConnectAuth(_h.env.root), 305 | _SSLAuthFailedHandlerFactory(_h), 306 | ssl_ctx 307 | where keepalive_timeout_secs = U32(2) 308 | ) 309 | let req = Payload.request( 310 | "GET", 311 | URL.build("https://" + host + ":" + port + "/bla")?) 312 | req.add_chunk("CHUNK") 313 | client(consume req)? 314 | else 315 | _h.fail("request building failed") 316 | end 317 | else 318 | _h.fail("unable to get port") 319 | end 320 | 321 | fun ref not_listening(listen: TCPListener ref) => 322 | _h.fail_action("server listening") 323 | _h.log("not_listening") 324 | 325 | fun ref closed(listen: TCPListener ref) => 326 | _h.log("TCP listener closed") 327 | 328 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ ? => 329 | _h.log("server listen connected.") 330 | let tcp_notify = 331 | object iso is TCPConnectionNotify 332 | fun ref received(conn: TCPConnection ref, data: Array[U8] iso, 333 | times: USize): Bool 334 | => 335 | _h.log("received on server") 336 | conn.write(consume data) 337 | true 338 | fun ref accepted(conn: TCPConnection ref) => 339 | _h.log("accepted on server") 340 | fun ref connect_failed(conn: TCPConnection ref) => 341 | _h.log("connect failed on server") 342 | fun ref closed(conn: TCPConnection ref) => 343 | _h.log("closed on server") 344 | fun ref auth_failed(conn: TCPConnection ref) => 345 | _h.log("auth failed on server") 346 | end 347 | let server_ssl_ctx = SSLContext.>set_cert( 348 | cert_path as FilePath, 349 | key_path as FilePath)? 350 | SSLConnection(consume tcp_notify, server_ssl_ctx.server()?) 351 | end 352 | 353 | let host = "127.0.0.1" 354 | let service = "0" 355 | 356 | let listener = TCPListener.ip4(TCPListenAuth(h.env.root), consume notify, host, service) 357 | h.dispose_when_done(listener) 358 | -------------------------------------------------------------------------------- /http/http.pony: -------------------------------------------------------------------------------- 1 | """ 2 | This package includes all the support functions necessary to build client 3 | applications for the HTTP protocol. 4 | 5 | The important interfaces an application needs to deal with are: 6 | 7 | * [HTTPSession](/http/http-HTTPSession/), the API to an HTTP connection. 8 | 9 | * [HTTPHandler](/http/http-HTTPHandler/), the interface to a handler you 10 | need to write that will receive notifications from the `HTTPSession`. 11 | 12 | * [HandlerFactory](/http/http-HandlerFactory/), the interface to a class you 13 | need to write that creates instances of your `HTTPHandler`. 14 | 15 | * [Payload](/http/http-Payload/), the class that represents a single HTTP 16 | message, with its headers. 17 | 18 | If you are writing a client, you will need to deal with the 19 | [HTTPClient](/http/http-HTTPClient/) class. 20 | 21 | If you are writing a server, you should look at the 22 | [Server](https://ponylang.github.io/http_server/http_server-Server/) 23 | class in the [http_server](https://ponylang.github.io/http_server) package. 24 | 25 | """ 26 | -------------------------------------------------------------------------------- /http/http_client.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "net" 3 | use "net_ssl" 4 | 5 | class HTTPClient 6 | """ 7 | Manages a group of HTTP connections on behalf of a client application. 8 | A client should create one instance of this class. 9 | """ 10 | let _auth: TCPConnectAuth 11 | let _sslctx: SSLContext 12 | let _pipeline: Bool 13 | let _keepalive_timeout_secs: U32 14 | let _sessions: Map[_HostService, _ClientConnection] = _sessions.create() 15 | let _handlermaker: HandlerFactory val 16 | 17 | new create( 18 | auth: TCPConnectAuth, 19 | handlermaker: HandlerFactory val, 20 | sslctx: (SSLContext | None) = None, 21 | pipeline: Bool = true, 22 | keepalive_timeout_secs: U32 = 0) 23 | => 24 | """ 25 | Create the context in which all HTTP sessions will originate. The `handlermaker` 26 | is used to create the `HTTPHandler` that is applied with each received 27 | payload after making a request. All requests made with one client are created 28 | using the same handler factory, if you need different handlers for different 29 | requests, you need to create different clients. 30 | 31 | Parameters: 32 | 33 | - keepalive_timeout_secs: Use TCP Keepalive and check if the other side is down 34 | every `keepalive_timeout_secs` seconds. 35 | """ 36 | _auth = auth 37 | 38 | _sslctx = try 39 | sslctx as SSLContext 40 | else 41 | recover 42 | let newssl = SSLContext 43 | newssl.set_client_verify(false) 44 | newssl 45 | end 46 | end 47 | 48 | _pipeline = pipeline 49 | _keepalive_timeout_secs = keepalive_timeout_secs 50 | _handlermaker = handlermaker 51 | 52 | fun ref apply(request: Payload trn) : Payload val ? => 53 | """ 54 | Schedule a request on an HTTP session. If a new connection is created, 55 | a new instance of the application's Receive Handler will be created 56 | for it. A `val` copy of the `Payload` is returned, and it can not be 57 | modified after this point. 58 | This is useful in Stream and Chunked transfer modes, so that the 59 | application can follow up with calls to `Client.send_body`. 60 | """ 61 | let session = _get_session(request.url)? 62 | let mode = request.transfer_mode 63 | request.session = session 64 | let valrequest: Payload val = consume request 65 | session(valrequest) 66 | valrequest 67 | 68 | fun ref dispose() => 69 | """ 70 | Disposes the sessions and cancels all pending requests. 71 | """ 72 | for s in _sessions.values() do 73 | s.dispose() 74 | end 75 | _sessions.clear() 76 | 77 | /* 78 | fun ref cancel(request: Payload val) => 79 | """ 80 | Cancel a request. 81 | """ 82 | match request.session 83 | | let s _ClientConnection tag => s.cancel(request) 84 | end 85 | */ 86 | 87 | fun ref _get_session(url: URL) : _ClientConnection ? => 88 | """ 89 | Gets or creates an HTTP Session for the given URL. If a new session 90 | is created, a new Receive Handler instance is created too. 91 | """ 92 | let hs = _HostService(url.scheme, url.host, url.port.string()) 93 | 94 | try 95 | // Look for an existing session 96 | _sessions(hs)? 97 | else 98 | // or create a new session of the correct type. 99 | let session = 100 | match url.scheme 101 | | "http" => 102 | _ClientConnection(_auth, hs.host, hs.service, 103 | None, _pipeline, _keepalive_timeout_secs, _handlermaker) 104 | | "https" => 105 | _ClientConnection(_auth, hs.host, hs.service, 106 | _sslctx, _pipeline, _keepalive_timeout_secs, _handlermaker) 107 | else 108 | error 109 | end 110 | _sessions(hs) = session 111 | session 112 | end 113 | 114 | fun ref send_body(data: ByteSeq val, session: HTTPSession) => 115 | session.write(data) 116 | 117 | class _SessionGuard 118 | """ 119 | Enforces the rule that an 'unsafe' request monopolizes the 120 | HTTPSession. A 'safe' request does not modify a resource state on 121 | the server, and such a request has no body. 122 | """ 123 | let _session: HTTPSession 124 | let _sent: List[Payload val] = List[Payload val] 125 | var _lastreqsafe: Bool = true 126 | var current: (Payload val | None) = None 127 | 128 | new iso create(session: HTTPSession) => 129 | _session = session 130 | 131 | fun ref submit(request: Payload val) ? => 132 | """ 133 | Accept a request for transmission to the server. This will fail if 134 | the request is not "safe" and the HTTPSession is busy. Due to the 135 | possibly large body size, these requests can not be queued. 136 | """ 137 | let thisreqsafe = request.is_safe() 138 | 139 | // If the channel is idle, just send this request. 140 | if _sent.size() == 0 then 141 | _lastreqsafe = thisreqsafe 142 | current = request 143 | _session(request) 144 | return 145 | end 146 | 147 | // Channel is not idle. But we can send it anyway if 148 | // both the last request and this one are safe. 149 | if _lastreqsafe and thisreqsafe then 150 | _sent.push(request) 151 | _session(request) 152 | return 153 | end 154 | 155 | // Channel can not accept another request now. 156 | error 157 | -------------------------------------------------------------------------------- /http/http_handler.pony: -------------------------------------------------------------------------------- 1 | primitive AuthFailed 2 | """ 3 | HTTP failure reason for when SSL Authentication failed. 4 | 5 | This failure reason is only reported to HTTP client HTTPHandler instances. 6 | """ 7 | 8 | primitive ConnectionClosed 9 | """ 10 | HTTP failure reason for when the connection was closed 11 | either from the other side (detectable when using TCP keepalive) 12 | or locally (e.g. due to an error). 13 | """ 14 | primitive ConnectFailed 15 | """ 16 | HTTP failure reason for when a connection could not be established. 17 | 18 | This failure reason is only valid for HTTP client HTTPHandlers. 19 | """ 20 | 21 | type HTTPFailureReason is ( 22 | AuthFailed | 23 | ConnectionClosed | 24 | ConnectFailed 25 | ) 26 | """ 27 | HTTP failure reason reported to `HTTPHandler.failed()`. 28 | """ 29 | 30 | interface HTTPHandler 31 | """ 32 | This is the interface through which HTTP messages are delivered *to* 33 | application code. On the server, this will be HTTP Requests (GET, 34 | HEAD, DELETE, POST, etc) sent from a client. On the client, this will 35 | be the HTTP Responses coming back from the server. The protocol is largely 36 | symmetrical and the same interface definition is used, though what 37 | processing happens behind the interface will of course vary. 38 | 39 | This interface delivers asynchronous events when receiving an HTTP 40 | message (called a `Payload`). Calls to these methods are made in 41 | the context of the `HTTPSession` actor so most of them should be 42 | passing data on to a processing actor. 43 | 44 | Each `HTTPSession` must have a unique instance of the handler. The 45 | application code does not necessarily know when an `HTTPSession` is created, 46 | so the application must provide an instance of `HandlerFactory` that 47 | will be called at the appropriate time. 48 | """ 49 | fun ref apply(payload: Payload val): Any => 50 | """ 51 | Notification of an incoming message. On the client, these will 52 | be responses coming from the server. On the server these will be requests 53 | coming from the client. The `Payload` object carries HTTP headers 54 | and the method, URL, and status codes. 55 | 56 | Only one HTTP message will be processed at a time, and that starts 57 | with a call to this method. This would be a good time to create 58 | an actor to deal with subsequent information pertaining to this 59 | message. 60 | """ 61 | 62 | fun ref chunk(data: ByteSeq val) => 63 | """ 64 | Notification of incoming body data. The body belongs to the most 65 | recent `Payload` delivered by an `apply` notification. 66 | """ 67 | 68 | fun ref finished() => 69 | """ 70 | Notification that no more body chunks are coming. Delivery of this HTTP 71 | message is complete. 72 | """ 73 | 74 | fun ref cancelled() => 75 | """ 76 | Notification that transferring the payload has been cancelled locally, 77 | e.g. by disposing the client, closing the server or manually cancelling a single request. 78 | """ 79 | 80 | fun ref failed(reason: HTTPFailureReason) => 81 | """ 82 | Notification about failure to transfer the payload 83 | (e.g. connection could not be established, authentication failed, connection was closed prematurely, ...) 84 | """ 85 | 86 | fun ref throttled() => 87 | """ 88 | Notification that the session temporarily can not accept more data. 89 | """ 90 | 91 | fun ref unthrottled() => 92 | """ 93 | Notification that the session can resume accepting data. 94 | """ 95 | 96 | fun ref need_body() => 97 | """ 98 | Notification that the HTTPSession is ready for Stream or Chunked 99 | body data. 100 | """ 101 | 102 | interface HandlerFactory 103 | """ 104 | The TCP connections that underlie HTTP sessions get created within 105 | the `http` package at times that the application code can not 106 | predict. Yet, the application code has to provide custom hooks into 107 | these connections as they are created. To accomplish this, the 108 | application code provides an instance of a `class` that implements 109 | this interface. 110 | 111 | The `HandlerFactory.apply` method will be called when a new 112 | `HTTPSession` is created, giving the application a chance to create 113 | an instance of its own `HTTPHandler`. This happens on both 114 | client and server ends. 115 | """ 116 | 117 | fun apply(session: HTTPSession): HTTPHandler ref^ 118 | """ 119 | Called by the `HTTPSession` when it needs a new instance of the 120 | application's `HTTPHandler`. It is suggested that the 121 | `session` value be passed to the constructor for the new 122 | `HTTPHandler` so that it is available for making 123 | `throttle` and `unthrottle` calls. 124 | """ 125 | -------------------------------------------------------------------------------- /http/http_parser.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use "net" 3 | use "encode/base64" 4 | 5 | // The parser internal state indicates what it expects to see next 6 | // in the input stream. 7 | 8 | primitive _ExpectRequest 9 | primitive _ExpectResponse 10 | primitive _ExpectHeaders 11 | primitive _ExpectContentLength 12 | primitive _ExpectChunkStart 13 | primitive _ExpectChunk 14 | primitive _ExpectChunkEnd 15 | primitive _ExpectBody 16 | primitive _ExpectReady 17 | primitive _ExpectError 18 | 19 | type _PayloadState is 20 | ( _ExpectRequest // Request method and URL 21 | | _ExpectResponse // Response status 22 | | _ExpectHeaders // More headers 23 | | _ExpectContentLength // Body text, limited by Content-Length 24 | | _ExpectChunkStart // The start of a 'chunked' piece of body text 25 | | _ExpectChunk // More of a continuing body 'chunk' 26 | | _ExpectChunkEnd // The CRLF at the end of a 'chunk' 27 | | _ExpectBody // Any body, which might not be there 28 | | _ExpectReady // All done with the message 29 | | _ExpectError // Not valid HTTP format 30 | ) 31 | 32 | primitive ParseError 33 | 34 | class HTTPParser 35 | """ 36 | This is the HTTP parser that builds a message `Payload` object 37 | representing either a Request or a Response from received chunks of data. 38 | """ 39 | let _client: Bool 40 | let _session: HTTPSession 41 | var _state: _PayloadState // Parser state 42 | var _payload: Payload iso // The Payload under construction 43 | var _expected_length: USize = 0 44 | var _transfer_mode: TransferMode = OneshotTransfer 45 | var _chunk_end: Bool = false 46 | var _delivered: Bool = false 47 | 48 | new request(session': HTTPSession) => 49 | """ 50 | Expect HTTP requests to arrive on a session. 51 | """ 52 | _client = false 53 | _session = session' 54 | _payload = Payload.request() 55 | _expected_length = 0 56 | _transfer_mode = OneshotTransfer 57 | _chunk_end = false 58 | _state = _ExpectRequest 59 | 60 | new response(session': HTTPSession) => 61 | """ 62 | Expect HTTP responses to arrive on a session. 63 | """ 64 | _client = true 65 | _session = session' 66 | _payload = Payload.response() 67 | _expected_length = 0 68 | _transfer_mode = OneshotTransfer 69 | _chunk_end = false 70 | _state = _ExpectResponse 71 | 72 | fun ref parse(buffer: Reader): (ParseError | None) => 73 | """ 74 | Analyze new data based on the parser's current internal state. 75 | """ 76 | match _state 77 | | _ExpectRequest => _parse_request(buffer) 78 | | _ExpectResponse => _parse_response(buffer) 79 | | _ExpectHeaders => _parse_headers(buffer) 80 | | _ExpectBody => 81 | // We are expecting a message body. Now we decide exactly 82 | // which encoding to look for. 83 | match _transfer_mode 84 | | ChunkedTransfer => 85 | _state = _ExpectChunkStart 86 | _parse_chunk_start(buffer) 87 | else 88 | _state = _ExpectContentLength 89 | _parse_content_length(buffer) 90 | end 91 | | _ExpectChunkStart => _parse_chunk_start(buffer) 92 | | _ExpectChunk => _parse_chunk(buffer) 93 | | _ExpectChunkEnd => _parse_chunk_end(buffer) 94 | | _ExpectContentLength => _parse_content_length(buffer) 95 | end 96 | 97 | fun ref _deliver() => 98 | """ 99 | The parser is finished with the message headers so we can push it 100 | to the `HTTPSession`. The body may come later. 101 | """ 102 | let body_follows = match _payload.transfer_mode 103 | | ChunkedTransfer => true 104 | else 105 | (_expected_length > 0) 106 | end 107 | 108 | // Set up `_payload` for the next message. 109 | let payload = _payload = Payload._empty(_client) 110 | _session._deliver(consume payload) 111 | if not body_follows then 112 | restart() 113 | end 114 | 115 | fun ref restart() => 116 | """ 117 | Restart parser state for the next message. It will be of the same 118 | kind as the last one. 119 | """ 120 | _expected_length = 0 121 | _transfer_mode = OneshotTransfer 122 | _chunk_end = false 123 | 124 | _state = if _client then 125 | _ExpectResponse 126 | else 127 | _ExpectRequest 128 | end 129 | 130 | fun ref closed(buffer: Reader) => 131 | """ 132 | The connection has closed, which may signal that all remaining data is the 133 | payload body. 134 | """ 135 | if _state is _ExpectBody then 136 | _expected_length = buffer.size() 137 | 138 | try 139 | let bytes = buffer.block(_expected_length)? 140 | let chunk: ByteSeq = recover val consume bytes end 141 | match _payload.transfer_mode 142 | | OneshotTransfer => _payload.add_chunk(chunk) 143 | else 144 | _session._chunk(chunk) 145 | end 146 | _state = _ExpectReady 147 | end 148 | end 149 | 150 | fun ref _parse_request(buffer: Reader): (ParseError | None) => 151 | """ 152 | Look for " ", the first line of an HTTP 153 | 'request' message. 154 | """ 155 | // Reset expectations 156 | _expected_length = 0 157 | _transfer_mode = OneshotTransfer 158 | _payload.session = _session 159 | 160 | try 161 | let line = buffer.line()? 162 | let method_end = line.find(" ")? 163 | _payload.method = line.substring(0, method_end) 164 | 165 | let url_end = line.find(" ", method_end + 1)? 166 | _payload.url = URL.valid(line.substring(method_end + 1, url_end))? 167 | _payload.proto = line.substring(url_end + 1) 168 | 169 | _state = _ExpectHeaders 170 | parse(buffer) 171 | else 172 | ParseError 173 | end 174 | 175 | fun ref _parse_response(buffer: Reader): (ParseError | None) => 176 | """ 177 | Look for " ", the first line of an 178 | HTTP 'response' message. 179 | """ 180 | // Reset expectations 181 | _expected_length = 0 182 | _transfer_mode = OneshotTransfer 183 | _payload.session = _session 184 | 185 | try 186 | let line = buffer.line()? 187 | 188 | let proto_end = line.find(" ")? 189 | _payload.proto = line.substring(0, proto_end) 190 | _payload.status = line.read_int[U16](proto_end + 1)?._1 191 | 192 | let status_end = line.find(" ", proto_end + 1)? 193 | _payload.method = line.substring(status_end + 1) 194 | 195 | _state = _ExpectHeaders 196 | parse(buffer) 197 | else 198 | ParseError 199 | end 200 | 201 | fun ref _parse_headers(buffer: Reader): (ParseError | None) => 202 | """ 203 | Look for: ":" or the empty line that marks the end of 204 | all the headers. 205 | """ 206 | while true do 207 | // Try to get another line out of the available buffer. 208 | // If this fails it is not a syntax error; we just wait for more. 209 | try 210 | let line = buffer.line()? 211 | if line.size() == 0 then 212 | // An empty line marks the end of the headers. Set state 213 | // appropriately. 214 | _set_header_end() 215 | 216 | // deliver for empty responses, chunked or streamed transfer 217 | // accumulate the body in the Payload for OneshotTransfer 218 | match _payload.transfer_mode 219 | | OneshotTransfer if _state isnt _ExpectBody => _deliver() 220 | | StreamTransfer => _deliver() 221 | | ChunkedTransfer => _deliver() 222 | end 223 | parse(buffer) 224 | else 225 | // A non-empty line *must* be a header. error if not. 226 | try 227 | _process_header(consume line)? 228 | else 229 | _state = _ExpectError 230 | break 231 | end 232 | end // line-size check 233 | else 234 | // Failed to get a line. We stay in _ExpectHeader state. 235 | return 236 | end // try 237 | end // looping over all headers in this buffer 238 | 239 | // Breaking out of that loop means an error. 240 | if _state is _ExpectError then ParseError end 241 | 242 | fun ref _process_header(line: String) ? => 243 | """ 244 | Save a header value. Raise an error on not finding the colon 245 | or can't interpret the value. 246 | """ 247 | let i = line.find(":")? 248 | let key = line.substring(0, i) 249 | key.>strip().lower_in_place() 250 | let value = line.substring(i + 1) 251 | value.strip() 252 | let value2: String val = consume value 253 | 254 | // Examine certain headers describing the encoding. 255 | if key.eq("content-length") then 256 | _expected_length = value2.read_int[USize]()?._1 257 | // On the receiving end, there is no difference 258 | // between Oneshot and Stream transfers except how 259 | // we store it. TODO eliminate this? 260 | _transfer_mode = 261 | if _expected_length > 10_000 then 262 | StreamTransfer 263 | else 264 | OneshotTransfer 265 | end 266 | _payload.transfer_mode = _transfer_mode 267 | elseif key.eq("transfer-encoding") then 268 | try 269 | value2.find("chunked")? 270 | _transfer_mode = ChunkedTransfer 271 | _payload.transfer_mode = _transfer_mode 272 | else 273 | _state = _ExpectError 274 | end 275 | elseif key.eq("host") then 276 | // TODO: set url host and service 277 | None 278 | elseif key.eq("authorization") then 279 | _setauth(value2) 280 | end 281 | 282 | _payload(consume key) = value2 283 | 284 | fun ref _setauth(auth: String) => 285 | """ 286 | Fill in username and password from an authentication header. 287 | """ 288 | try 289 | let parts = auth.split(" ") 290 | let authscheme = parts(0)? 291 | match authscheme.lower() 292 | | "basic" => 293 | let autharg = parts(1)? 294 | let userpass = Base64.decode[String iso](autharg)? 295 | let uparts = userpass.split(":") 296 | _payload.username = uparts(0)? 297 | _payload.password = uparts(1)? 298 | end 299 | end 300 | 301 | fun ref _set_header_end() => 302 | """ 303 | Line size is zero, so we have reached the end of the headers. 304 | Certain status codes mean there is no body. 305 | """ 306 | if 307 | (_payload.status == 204) // no content 308 | or (_payload.status == 304) // not modified 309 | or ((_payload.status > 0) and (_payload.status < 200)) 310 | then 311 | _state = _ExpectReady 312 | else 313 | // If chunked mode or length>0 then some body data will follow. 314 | // In any case we can pass the completed `Payload` on to the 315 | // session for processing. 316 | _state = match _payload.transfer_mode 317 | | ChunkedTransfer => 318 | _ExpectChunkStart 319 | else 320 | if _expected_length == 0 then 321 | _ExpectReady 322 | else 323 | _ExpectBody 324 | end 325 | end 326 | end // else no special status 327 | 328 | fun ref _parse_content_length(buffer: Reader) => 329 | """ 330 | Look for `_expected_length` bytes set by having seen a `Content-Length` 331 | header. We may not see it all at once but we process the lesser of 332 | what we need and what is available in the buffer. 333 | """ 334 | let available = buffer.size() 335 | let usable = available.min(_expected_length) 336 | 337 | try 338 | let bytes = buffer.block(usable)? 339 | let body = recover val consume bytes end 340 | _expected_length = _expected_length - usable 341 | // in streaming mode we already have a new unrelated payload in _payload 342 | // so we need to keep track of the current transfer-mode via _transfer_mode 343 | match _transfer_mode 344 | | OneshotTransfer => 345 | // in oneshot transfer we actually fill the body of the payload 346 | _payload.add_chunk(body) 347 | else 348 | _session._chunk(body) 349 | end 350 | 351 | // All done with this message if we have processed the entire body. 352 | if _expected_length == 0 then 353 | match _transfer_mode 354 | | OneshotTransfer => 355 | // we have all the body, finally deliver it 356 | _deliver() 357 | else 358 | // explicitly finish the session in chunked and stream mode 359 | _session._finish() 360 | end 361 | restart() 362 | end 363 | end 364 | 365 | fun ref _parse_chunk_start(buffer: Reader): (ParseError | None) => 366 | """ 367 | Look for the beginning of a chunk, which is a length in hex on a line 368 | terminated by CRLF. An explicit length of zero marks the end of 369 | the entire chunked message body. 370 | """ 371 | let line = try 372 | buffer.line()? 373 | else 374 | return ParseError 375 | end 376 | 377 | if line.size() > 0 378 | then 379 | // This should be the length of the next chunk. 380 | _expected_length = try 381 | line.read_int[USize](0, 16)?._1 382 | else 383 | return ParseError 384 | end 385 | // A chunk explicitly of length zero marks the end of the body. 386 | if _expected_length > 0 then 387 | _state = _ExpectChunk 388 | else 389 | // We already have the CRLF after the zero, so we are all done. 390 | _session._finish() 391 | restart() 392 | end 393 | 394 | parse(buffer) 395 | else 396 | // Anything other than a length is an error. 397 | _expected_length = 0 398 | _state = _ExpectError 399 | ParseError 400 | end 401 | 402 | fun ref _parse_chunk(buffer: Reader) => 403 | """ 404 | Look for a chunk of the size set by `_parse_chunk_start`. We may 405 | not see it all at once but we process the lesser of what we need 406 | and what is available in the buffer. ChunkedTransfer mode always 407 | delivers directly to the HTTPSession handler. 408 | """ 409 | let available = buffer.size() 410 | let usable = available.min(_expected_length) 411 | try 412 | let chunk = buffer.block(usable)? 413 | _session._chunk(consume chunk) 414 | _expected_length = _expected_length - usable 415 | 416 | // If we have all of the chunk, look for the trailing CRLF. 417 | // Otherwise we will keep working on this chunk. 418 | if _expected_length == 0 then 419 | _state = _ExpectChunkEnd 420 | parse(buffer) 421 | end 422 | end 423 | 424 | fun ref _parse_chunk_end(buffer: Reader) => 425 | """ 426 | Look for the CRLF that ends every chunk. AFter that we look for 427 | the next chunk, or that was the special ending chunk. 428 | """ 429 | try 430 | let line = buffer.line()? 431 | if _chunk_end 432 | then 433 | _session._finish() 434 | restart() 435 | else 436 | _state = _ExpectChunkStart 437 | parse(buffer) 438 | end 439 | end 440 | 441 | /* Saved for debugging. 442 | fun ref _say() => 443 | match _state 444 | | _ExpectRequest => Debug.out("-Request method and URL") 445 | | _ExpectResponse => Debug.out("-Response status") 446 | | _ExpectHeaders => Debug.out("-More headers") 447 | | _ExpectContentLength => 448 | Debug.out("-Body text, limited by Content-Length") 449 | | _ExpectChunkStart => 450 | Debug.out("-The start of a 'chunked' piece of body text") 451 | | _ExpectChunk => Debug.out("-More of a continuing body 'chunk'") 452 | | _ExpectChunkEnd => Debug.out("-The CRLF at the end of a 'chunk'") 453 | | _ExpectBody => Debug.out("-Any body, which might not be there") 454 | | _ExpectReady => Debug.out("-All done with the message") 455 | | _ExpectError => Debug.out("-Not valid HTTP format") 456 | end 457 | */ 458 | -------------------------------------------------------------------------------- /http/http_session.pony: -------------------------------------------------------------------------------- 1 | trait tag HTTPSession 2 | """ 3 | An HTTP Session is the external API to the communication link 4 | between client and server. A session can only transfer one message 5 | at a time in each direction. The client and server each have their 6 | own ways of implementing this interface, but to application code (either 7 | in the client or in the server 'back end') this interface provides a 8 | common view of how information is passed *into* the `http` package. 9 | """ 10 | be apply(payload: Payload val) 11 | """ 12 | Start sending a request or response. The `Payload` must have all its 13 | essential fields filled in at this point, because ownership is being 14 | transferred to the session actor. This begins an outbound message. 15 | """ 16 | 17 | be finish() 18 | """ 19 | Indicate that all *outbound* `add_chunk` calls have been made and 20 | submission of the HTTP message is complete. 21 | """ 22 | 23 | be dispose() 24 | """ 25 | Close the connection from this end. 26 | """ 27 | 28 | be write(data: ByteSeq val) 29 | """ 30 | Write raw byte stream to the outbound TCP connection. 31 | """ 32 | 33 | be _mute() 34 | """ 35 | Stop delivering *incoming* data to the handler. This may not 36 | be effective instantly. 37 | """ 38 | 39 | be _unmute() 40 | """ 41 | Resume delivering incoming data to the handler. 42 | """ 43 | 44 | be cancel(msg: Payload val) 45 | """ 46 | Tell the session to stop sending an *outbound* message. 47 | """ 48 | 49 | be _deliver(payload: Payload val) 50 | """ 51 | The appropriate Payload Builder will call this from the `TCPConnection` 52 | actor to start delivery of a new *inbound* message. If the `Payload`s 53 | `transfer_mode` is `OneshotTransfer`, this is the only notification 54 | that will happen for the message. Otherwise there will be one or more 55 | `_chunk` calls followed by a `_finish` call. 56 | """ 57 | 58 | be _chunk(data: ByteSeq val) 59 | """ 60 | Deliver a piece of *inbound* body data to the application `HTTPHandler` 61 | This is called by the PayloadBuilder. 62 | """ 63 | 64 | be _finish() 65 | """ 66 | Inidcates that the last *inbound* body chunk has been sent to 67 | `_chunk`. This is called by the PayloadBuilder. 68 | """ 69 | 70 | -------------------------------------------------------------------------------- /http/mimetypes.pony: -------------------------------------------------------------------------------- 1 | primitive MimeTypes 2 | """ 3 | Provide mapping from file names to MIME types. 4 | TODO load from /etc/mime.types 5 | """ 6 | 7 | fun apply(name: String): String val^ => 8 | """ 9 | Mapping is based on the file type, following the last period in the name. 10 | """ 11 | try 12 | // This will fail if no period is found. 13 | let dotpos = (name.rfind(".", -1, 0)? + 1).usize() 14 | 15 | match name.trim(dotpos).lower() 16 | | "html" => "text/html" 17 | | "jpg" => "image/jpeg" 18 | | "jpeg" => "image/jpeg" 19 | | "png" => "image/png" 20 | | "css" => "text/css" 21 | | "ico" => "image/x-icon" 22 | | "js" => "application/javascript" 23 | | "mp3" => "audio/mpeg3" 24 | | "m3u" => "audio/mpegurl" 25 | | "ogg" => "audio/ogg" 26 | | "doc" => "application/msword" 27 | | "gif" => "image/gif" 28 | | "txt" => "text/plain" 29 | | "wav" => "audio/wav" 30 | else 31 | "application/octet-stream" // None of the above 32 | end 33 | else 34 | "application/octet-stream" // No filetype 35 | end 36 | 37 | 38 | -------------------------------------------------------------------------------- /http/payload.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "net" 3 | use "format" 4 | use "buffered" 5 | 6 | primitive ChunkedTransfer 7 | primitive StreamTransfer 8 | primitive OneshotTransfer 9 | 10 | type TransferMode is (ChunkedTransfer | StreamTransfer | OneshotTransfer) 11 | 12 | class trn Payload 13 | """ 14 | This class represent a single HTTP message, which can be either a 15 | `request` or a `response`. 16 | 17 | ### Transfer Modes 18 | 19 | HTTP provides two ways to encode the transmission of a message 'body', 20 | of any size. This package supports both of them: 21 | 22 | 2. **StreamTransfer**. This is used for payload bodies where the exact 23 | length is known in advance, including most transfers of files. It is 24 | selected by calling `Payload.set_length` with an integer bytecount. 25 | Appication buffer sizes determine how much data is fed to the TCP 26 | connection at once, but the total amount must match this size. 27 | 28 | 3. **ChunkedTransfer**. This is used when the payload length can not be 29 | known in advance, but can be large. It is selected by calling 30 | `Payload.set_length` with a parameter of `None`. On the TCP link this mode 31 | can be detected because there is no `Content-Length` header at all, being 32 | replaced by the `Transfer-Encoding: chunked` header. In addition, the 33 | message body is separated into chunks, each with its own bytecount. As with 34 | `StreamTransfer` mode, transmission can be spread out over time with the 35 | difference that it is the original data source that determines the chunk 36 | size. 37 | 38 | If `Payload.set_length` is never called at all, a variation on 39 | `StreamTransfer` called `OneshotTransfer` is used. In this case, all of 40 | the message body is placed into the message at once, using 41 | `Payload.add_chunk` calls. The size will be determined when the message is 42 | submitted for transmission. Care must be taken not to consume too much 43 | memory, especially on a server where there can be multiple messages in 44 | transit at once. 45 | 46 | The type of transfer being used by an incoming message can be determined 47 | from its `transfer_mode` field, which will be one of the 48 | [TransferMode](/http/http-TransferMode) types. 49 | 50 | ### Sequence 51 | 52 | For example, to send a message of possibly large size: 53 | 54 | 1. Create the message with a call to `Payload.request` or `Payload.response`. 55 | 2. Set the `session` field of the message. 56 | 2. Call `Payload.set_length` to indicate the length of the body. 57 | 3. Add any additional headers that may be required, such as `Content-type`. 58 | 4. Submit the message for transmission by calling the either the 59 | `HTTPSession.apply` method (in servers) or the `HTTPCLient.apply` method 60 | in clients. 61 | 5. Wait for the `send_body` notification. 62 | 6. Make any number of calls to `Payload.send_chunk`. 63 | 7. Call `Payload.finish`. 64 | 65 | To send a message of small, reasonable size (say, under 20KB), this 66 | simplified method can be used instead: 67 | 68 | 1. Create the message with a call to `Payload.request` or `Payload.response`. 69 | 2. Set the `session` field of the message. 70 | 3. Add any additional headers that may be required, such as `Content-type`. 71 | 4. Call `add_chunk` one or more times to add body data. 72 | 4. Submit the message for transmission by calling the either the 73 | [HTTPSession](/http/http-HTTPSession)`.apply` method (in servers) or the 74 | [HTTPClient](/http/http-HTTPClient)`.apply` method in clients. 75 | """ 76 | var proto: String = "HTTP/1.1" 77 | """The HTTP protocol string""" 78 | 79 | var status: U16 80 | """ 81 | Internal representation of the response [Status](http-Status). 82 | 83 | Will be `0` for HTTP requests. 84 | """ 85 | 86 | var method: String 87 | """ 88 | The HTTP Method. 89 | 90 | `GET`, `POST`, `DELETE`, `OPTIONS`, ... 91 | 92 | For HTTP responses this will be the status string, 93 | for a `200` status this will be `200 OK`, for `404`, `404 Not Found` etc.. 94 | """ 95 | 96 | var url: URL 97 | """ 98 | The HTTP request [URL](http-URL). 99 | It will be used for the HTTP path and the `Host` header. 100 | The `user` and `password` fields are ignored. 101 | 102 | For HTTP responses this will be an empty [URL](http-URL). 103 | """ 104 | var _body_length: USize = 0 105 | var transfer_mode: TransferMode = OneshotTransfer 106 | """ 107 | Determines the transfer mode of this message. 108 | 109 | In case of outgoing requests or responses, 110 | use `set_length` to control the transfer mode. 111 | 112 | In case of incoming requests, this field determines 113 | how the request is transferred. 114 | """ 115 | var session: (HTTPSession | None) = None 116 | 117 | embed _headers: Map[String, String] = _headers.create() 118 | embed _body: Array[ByteSeq val] = _body.create() 119 | let _response: Bool 120 | var username: String = "" 121 | """ 122 | The username extracted from an `Authentication` header of an HTTP request 123 | received via [HTTPServer](https://ponylang.github.io/http_server/http_server-Server/). 124 | 125 | This is not used and not sent using [HTTPClient](http-HTTPClient), 126 | use `update` to set an `Authentication` header instead. 127 | """ 128 | var password: String = "" 129 | """ 130 | The password extracted from an `Authentication` header of an HTTP request 131 | received via [HTTPServer](https://ponylang.github.io/http_server/http_server-Server/). 132 | 133 | This is not used and not sent using [HTTPClient](http-HTTPClient), 134 | use `update` to set an `Authentication` header instead. 135 | """ 136 | 137 | new iso request(method': String = "GET", url': URL = URL) => 138 | """ 139 | Create an HTTP `request` message. 140 | """ 141 | status = 0 142 | method = method' 143 | url = url' 144 | _response = false 145 | 146 | new iso response(status': Status = StatusOK) => 147 | """ 148 | Create an HTTP `response` message. 149 | """ 150 | status = status'() 151 | method = status'.string() 152 | url = URL 153 | _response = true 154 | 155 | new iso _empty(response': Bool = true) => 156 | """ 157 | Create an empty HTTP payload. 158 | """ 159 | status = 0 160 | method = "" 161 | url = URL 162 | _response = response' 163 | 164 | fun apply(key: String): String ? => 165 | """ 166 | Get a header. 167 | """ 168 | _headers(key.lower())? 169 | 170 | fun is_safe(): Bool => 171 | """ 172 | A request method is "safe" if it does not modify state in the resource. 173 | These methods can be guaranteed not to have any body data. 174 | Return true for a safe request method, false otherwise. 175 | """ 176 | match method 177 | | "GET" 178 | | "HEAD" 179 | | "OPTIONS" => 180 | true 181 | else 182 | false 183 | end 184 | 185 | fun body(): this->Array[ByteSeq] ? => 186 | """ 187 | Get the body in `OneshotTransfer` mode. 188 | In the other modes it raises an error. 189 | """ 190 | match transfer_mode 191 | | OneshotTransfer => _body 192 | else error 193 | end 194 | 195 | fun ref set_length(bytecount: (USize | None)) => 196 | """ 197 | Set the body length when known in advance. This determines the 198 | transfer mode that will be used. A parameter of 'None' will use 199 | Chunked Transfer Encoding. A numeric value will use Streamed 200 | transfer. Not calling this function at all will 201 | use Oneshot transfer. 202 | """ 203 | match bytecount 204 | | None => 205 | transfer_mode = ChunkedTransfer 206 | _headers("Transfer-Encoding") = "chunked" 207 | | let n: USize => 208 | try not _headers.contains("Content-Length") then 209 | _headers("Content-Length") = n.string() 210 | end 211 | _body_length = n 212 | transfer_mode = StreamTransfer 213 | end 214 | 215 | fun ref update(key: String, value: String): Payload ref^ => 216 | """ 217 | Set any header. If we've already received the header, append the value as a 218 | comma separated list, as per RFC 2616 section 4.2. 219 | """ 220 | _headers.upsert(key.lower(), 221 | value, 222 | {(current, provided) => current + "," + provided}) 223 | this 224 | 225 | fun headers(): this->Map[String, String] => 226 | """ 227 | Get all the headers. 228 | """ 229 | _headers 230 | 231 | fun body_size(): (USize | None) => 232 | """ 233 | Get the total intended size of the body. 234 | `ServerConnection` accumulates actual size transferred for logging. 235 | """ 236 | match transfer_mode 237 | | ChunkedTransfer => None 238 | else _body_length 239 | end 240 | 241 | fun ref add_chunk(data: ByteSeq val): Payload ref^ => 242 | """ 243 | This is how application code adds data to the body in 244 | `OneshotTransfer` mode. For large bodies, call `set_length` 245 | and use `send_chunk` instead. 246 | """ 247 | _body.push(data) 248 | _body_length = _body_length + data.size() 249 | 250 | this 251 | 252 | fun box send_chunk(data: ByteSeq val) => 253 | """ 254 | This is how application code sends body data in `StreamTransfer` and 255 | `ChunkedTransfer` modes. For smaller body lengths, `add_chunk` in 256 | `Oneshot` mode can be used instead. 257 | """ 258 | match session 259 | | let s: HTTPSession => 260 | match transfer_mode 261 | | ChunkedTransfer => 262 | // Wrap some body data in the Chunked Transfer Encoding format, 263 | // which is the length in hex, the data, and a CRLF. It is 264 | // important to never send a chunk of length zero, as that is 265 | // how the end of the body is signalled. 266 | s.write(Format.int[USize](data.size(), FormatHexBare)) 267 | s.write("\r\n") 268 | s.write(data) 269 | s.write("\r\n") 270 | | StreamTransfer => 271 | // In stream mode just send the data. Its length should have 272 | // already been accounted for by `set_length`. 273 | s.write(data) 274 | end 275 | end 276 | 277 | fun val finish() => 278 | """ 279 | Mark the end of body transmission. This does not do anything, 280 | and is unnecessary, in Oneshot mode. 281 | """ 282 | match session 283 | | let s: HTTPSession => 284 | match transfer_mode 285 | | ChunkedTransfer => 286 | s.write("0\r\n\r\n") 287 | s.finish() 288 | | StreamTransfer => 289 | s.finish() 290 | end 291 | end 292 | 293 | fun val respond(response': Payload) => 294 | """ 295 | Start sending a response from the server to the client. 296 | """ 297 | try 298 | (session as HTTPSession)(consume response') 299 | end 300 | 301 | fun val _client_fail() => 302 | """ 303 | Start sending an error response. 304 | """ 305 | None 306 | /* Not sure if we need this. Nobody calls it. But something like: 307 | try 308 | (session as HTTPSession)( 309 | Payload.response(StatusInternalServerError)) 310 | end 311 | */ 312 | 313 | fun val _write(keepalive: Bool = true, wr: Writer ref) => 314 | """ 315 | Writes the payload to a Writer. Requests and Responses differ 316 | only in the first line of text - everything after that is the same format. 317 | """ 318 | if _response then 319 | _write_response(keepalive, wr) 320 | else 321 | _write_request(keepalive, wr) 322 | end 323 | 324 | _write_common(wr) 325 | 326 | fun val _write_request(keepalive: Bool, wr: Writer ref) => 327 | """ 328 | Writes the 'request' parts of an HTTP message. 329 | """ 330 | wr 331 | .> write(method) 332 | .> write(" ") 333 | .> write(url.path) 334 | 335 | if url.query.size() > 0 then 336 | wr 337 | .> write("?") 338 | .> write(url.query) 339 | end 340 | 341 | if url.fragment.size() > 0 then 342 | wr 343 | .> write("#") 344 | .> write(url.fragment) 345 | end 346 | 347 | wr 348 | .> write(" ") 349 | .> write(proto) 350 | .> write("\r\n") 351 | 352 | if not keepalive then 353 | wr.write("Connection: close\r\n") 354 | end 355 | 356 | if url.port == url.default_port() then 357 | wr 358 | .> write("Host: ") 359 | .> write(url.host) 360 | .> write("\r\n") 361 | else 362 | wr 363 | .> write("Host: ") 364 | .> write(url.host) 365 | .> write(":") 366 | .> write(url.port.string()) 367 | .> write("\r\n") 368 | end 369 | 370 | fun val _write_common(wr: Writer ref) => 371 | """ 372 | Writes the parts of an HTTP message common to both requests and 373 | responses. 374 | """ 375 | _write_headers(wr) 376 | 377 | // In oneshot mode we send the entire stored body. 378 | if transfer_mode is OneshotTransfer then 379 | for piece in _body.values() do 380 | wr.write(piece) 381 | end 382 | end 383 | 384 | fun val _write_response(keepalive: Bool, wr: Writer ref) => 385 | """ 386 | Write the response-specific parts of an HTTP message. This is the 387 | status line, consisting of the protocol name, the status value, 388 | and a string representation of the status (carried in the `method` 389 | field). Since writing it out is an actor behavior call, we go to 390 | the trouble of packaging it into a single string before sending. 391 | """ 392 | 393 | wr 394 | .> write(proto) 395 | .> write(" ") 396 | .> write(status.string()) 397 | .> write(" ") 398 | .> write(method) 399 | .> write("\r\n") 400 | 401 | if keepalive then 402 | wr.write("Connection: keep-alive\r\n") 403 | end 404 | 405 | fun _write_headers(wr: Writer ref) => 406 | """ 407 | Write all of the HTTP headers to the Writer. 408 | """ 409 | var saw_length: Bool = false 410 | for (k, v) in _headers.pairs() do 411 | if (k != "Host") then 412 | if k == "Content-Length" then saw_length = true end 413 | wr 414 | .> write(k) 415 | .> write(": ") 416 | .> write(v) 417 | .> write("\r\n") 418 | end 419 | end 420 | 421 | if (not saw_length) and (transfer_mode is OneshotTransfer) then 422 | wr 423 | .> write("Content-Length: ") 424 | .> write(_body_length.string()) 425 | .> write("\r\n") 426 | end 427 | 428 | // Blank line before the body. 429 | wr.write("\r\n") 430 | 431 | fun box has_body(): Bool => 432 | """ 433 | Determines whether a message has a body portion. 434 | """ 435 | if _response then 436 | // Errors never have bodies. 437 | if 438 | (status == 204) // no content 439 | or (status == 304) // not modified 440 | or ((status > 0) and (status < 200)) 441 | or (status > 400) 442 | then 443 | false 444 | else 445 | true 446 | end 447 | else 448 | match transfer_mode 449 | | ChunkedTransfer => true 450 | else (_body_length > 0) 451 | end 452 | end 453 | -------------------------------------------------------------------------------- /http/status.pony: -------------------------------------------------------------------------------- 1 | trait val Status 2 | fun apply(): U16 3 | fun string(): String 4 | 5 | primitive StatusContinue is Status 6 | fun apply(): U16 => 100 7 | fun string(): String => "100 Continue" 8 | primitive StatusSwitchingProtocols is Status 9 | fun apply(): U16 => 101 10 | fun string(): String => "101 Switching Protocols" 11 | 12 | primitive StatusOK is Status 13 | fun apply(): U16 => 200 14 | fun string(): String => "200 OK" 15 | primitive StatusCreated is Status 16 | fun apply(): U16 => 201 17 | fun string(): String => "201 Created" 18 | primitive StatusAccepted is Status 19 | fun apply(): U16 => 202 20 | fun string(): String => "202 Accepted" 21 | primitive StatusNonAuthoritativeInfo is Status 22 | fun apply(): U16 => 203 23 | fun string(): String => "203 Non-Authoritative Information" 24 | primitive StatusNoContent is Status 25 | fun apply(): U16 => 204 26 | fun string(): String => "204 No Content" 27 | primitive StatusResetContent is Status 28 | fun apply(): U16 => 205 29 | fun string(): String => "205 Reset Content" 30 | primitive StatusPartialContent is Status 31 | fun apply(): U16 => 206 32 | fun string(): String => "206 Partial Content" 33 | 34 | primitive StatusMultipleChoices is Status 35 | fun apply(): U16 => 300 36 | fun string(): String => "300 Multiple Choices" 37 | primitive StatusMovedPermanently is Status 38 | fun apply(): U16 => 301 39 | fun string(): String => "301 Moved Permanently" 40 | primitive StatusFound is Status 41 | fun apply(): U16 => 302 42 | fun string(): String => "302 Found" 43 | primitive StatusSeeOther is Status 44 | fun apply(): U16 => 303 45 | fun string(): String => "303 See Other" 46 | primitive StatusNotModified is Status 47 | fun apply(): U16 => 304 48 | fun string(): String => "304 Not Modified" 49 | primitive StatusUseProxy is Status 50 | fun apply(): U16 => 305 51 | fun string(): String => "305 Use Proxy" 52 | primitive StatusTemporaryRedirect is Status 53 | fun apply(): U16 => 307 54 | fun string(): String => "307 Temporary Redirect" 55 | 56 | primitive StatusBadRequest is Status 57 | fun apply(): U16 => 400 58 | fun string(): String => "400 Bad Request" 59 | primitive StatusUnauthorized is Status 60 | fun apply(): U16 => 401 61 | fun string(): String => "401 Unauthorized" 62 | primitive StatusPaymentRequired is Status 63 | fun apply(): U16 => 402 64 | fun string(): String => "402 Payment Required" 65 | primitive StatusForbidden is Status 66 | fun apply(): U16 => 403 67 | fun string(): String => "403 Forbidden" 68 | primitive StatusNotFound is Status 69 | fun apply(): U16 => 404 70 | fun string(): String => "404 Not Found" 71 | primitive StatusMethodNotAllowed is Status 72 | fun apply(): U16 => 405 73 | fun string(): String => "405 Method Not Allowed" 74 | primitive StatusNotAcceptable is Status 75 | fun apply(): U16 => 406 76 | fun string(): String => "406 Not Acceptable" 77 | primitive StatusProxyAuthRequired is Status 78 | fun apply(): U16 => 407 79 | fun string(): String => "407 Proxy Authentication Required" 80 | primitive StatusRequestTimeout is Status 81 | fun apply(): U16 => 408 82 | fun string(): String => "408 Request Timeout" 83 | primitive StatusConflict is Status 84 | fun apply(): U16 => 409 85 | fun string(): String => "409 Conflict" 86 | primitive StatusGone is Status 87 | fun apply(): U16 => 410 88 | fun string(): String => "410 Gone" 89 | primitive StatusLengthRequired is Status 90 | fun apply(): U16 => 411 91 | fun string(): String => "411 Length Required" 92 | primitive StatusPreconditionFailed is Status 93 | fun apply(): U16 => 412 94 | fun string(): String => "412 Precondition Failed" 95 | primitive StatusRequestEntityTooLarge is Status 96 | fun apply(): U16 => 413 97 | fun string(): String => "413 Request Entity Too Large" 98 | primitive StatusRequestURITooLong is Status 99 | fun apply(): U16 => 414 100 | fun string(): String => "414 Request URI Too Long" 101 | primitive StatusUnsupportedMediaType is Status 102 | fun apply(): U16 => 415 103 | fun string(): String => "415 Unsupported Media Type" 104 | primitive StatusRequestedRangeNotSatisfiable is Status 105 | fun apply(): U16 => 416 106 | fun string(): String => "416 Requested Range Not Satisfiable" 107 | primitive StatusExpectationFailed is Status 108 | fun apply(): U16 => 417 109 | fun string(): String => "417 Expectation Failed" 110 | primitive StatusTeapot is Status 111 | fun apply(): U16 => 418 112 | fun string(): String => "418 I'm a teapot" 113 | primitive StatusPreconditionRequired is Status 114 | fun apply(): U16 => 428 115 | fun string(): String => "428 Precondition Required" 116 | primitive StatusTooManyRequests is Status 117 | fun apply(): U16 => 429 118 | fun string(): String => "429 Too Many Requests" 119 | primitive StatusRequestHeaderFieldsTooLarge is Status 120 | fun apply(): U16 => 431 121 | fun string(): String => "431 Request Header Fields Too Large" 122 | primitive StatusUnavailableForLegalReasons is Status 123 | fun apply(): U16 => 451 124 | fun string(): String => "451 Unavailable For Legal Reasons" 125 | 126 | primitive StatusInternalServerError is Status 127 | fun apply(): U16 => 500 128 | fun string(): String => "500 Internal Server Error" 129 | primitive StatusNotImplemented is Status 130 | fun apply(): U16 => 501 131 | fun string(): String => "501 Not Implemented" 132 | primitive StatusBadGateway is Status 133 | fun apply(): U16 => 502 134 | fun string(): String => "502 Bad Gateway" 135 | primitive StatusServiceUnavailable is Status 136 | fun apply(): U16 => 503 137 | fun string(): String => "503 Service Unavailable" 138 | primitive StatusGatewayTimeout is Status 139 | fun apply(): U16 => 504 140 | fun string(): String => "504 Gateway Timeout" 141 | primitive StatusHTTPVersionNotSupported is Status 142 | fun apply(): U16 => 505 143 | fun string(): String => "505 HTTP Version Not Supported" 144 | primitive StatusNetworkAuthenticationRequired is Status 145 | fun apply(): U16 => 511 146 | fun string(): String => "511 Network Authentication Required" 147 | -------------------------------------------------------------------------------- /http/test/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF8zCCA9ugAwIBAgIUXLv1TPBvnBHEkinzUfTGWH+mqUkwDQYJKoZIhvcNAQEL 3 | BQAwgYgxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJl 4 | cmxpbjERMA8GA1UECgwIcG9ueWxhbmcxETAPBgNVBAsMCHBvbnlsYW5nMRIwEAYD 5 | VQQDDAlsb2NhbGhvc3QxHTAbBgkqhkiG9w0BCQEWDnJvb3RAbG9jYWxob3N0MB4X 6 | DTE5MDcwMjE5NDQ0MFoXDTE5MDcwMzE5NDQ0MFowgYgxCzAJBgNVBAYTAkRFMQ8w 7 | DQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjERMA8GA1UECgwIcG9ueWxh 8 | bmcxETAPBgNVBAsMCHBvbnlsYW5nMRIwEAYDVQQDDAlsb2NhbGhvc3QxHTAbBgkq 9 | hkiG9w0BCQEWDnJvb3RAbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A 10 | MIICCgKCAgEA39KrU2f3Osgd9zWyyZz3Ot8fYKhCKPZ1V6wsJBcr/CQ2vrviuxHb 11 | UgrCT5XzgST1rRQcX2ZfB+OZoNT4L6nzaDA8EmeRLibIwexEIVOpi7DjxeOodAuE 12 | YMDDI0CX67RtJMcpCYES1zdm2k0o+Ezx15aosXla1OaAmGoG0tBW91O22My13Y+h 13 | UFiIvCnXuKEnxRiQgV12mK2o86fidpOjiaEHSTxwIJK7U/0ylfc/IPR0IrZMRGgs 14 | OZKjRImJke+HAxVmr8Dy0K+xS9fNazUOjAVbGPrDDnpPikH9vS/MkvaSynuN7Ie3 15 | 5jB1Jx0KGB+b2ut7RNeVb82FKmxfNj4Lzsg47TrEKkR8khFPr14xI95AjV0dfv/C 16 | 7zuUNL5GjhdWb5pDnZn4NyeGzAkOjM+8TpDuFSvbKik9TPPyQ9COZkD9LkDnWQQ6 17 | 1N6b2SmaMHhe5h5bP2B+5zVTvo3mmwYJWH0PKT8SPf+K6ZWC6hnClMW9ZDcHsHoA 18 | 4pNRmH57zndwrzQzHGrQ230jXjxFdEvke0Nt2Vuhy0XL5PTwbPT9Yf6YLhW/DaXI 19 | FzX1OdDJqKMWmm6Xpqy9TD0eTIpl3sUrYskomjILvaESYwxDKZI4nPXnn9yPo1Kf 20 | Q7BVJduyv2UQrlx6Bvda1Q5Kwp6g5B3HT4IVydUnEGEpPB9e9MVQGbsCAwEAAaNT 21 | MFEwHQYDVR0OBBYEFJ2Y/PJbR1iXZNgiYI5KPG3RCCG1MB8GA1UdIwQYMBaAFJ2Y 22 | /PJbR1iXZNgiYI5KPG3RCCG1MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL 23 | BQADggIBAI9RAK5Bh9mwjqB4k+fI39vA+BOgqJnCzaTeNp7NuOiCP7+Wzjkay8tP 24 | EwaRXheamdZ8sWEXPnnrAbMJyXbZ9eol9nK1kwGQ9/lZ+iOAUeYSTt5BF6EqjDwK 25 | 6s8UDuyY4DXdkJp1l1pZkuyfWtQ4ZFcrhHOhZIs9ZlNLxmx91AmB+LS1p7lRdcUs 26 | Ktc5LAoqWsTP4JeYi99ZFWmGzwZl/mvKpx3CeMnd1G3Mhb8wdwhbz3IlEcCH6cuz 27 | UFB4jRmRskGrZk5935G8Q9Ar3+X5dOavzpH7UvgBpVM6qzy8lpKYpd8iocxJiYGr 28 | mzmeK1jPEnxU6vXXnXQgPQiinAItAbn9GU9+g1UfBp1910AOUv0Xz63bMt81P4NP 29 | maCd9kFdONU8LQGfgCYU/eSSIJRvy+Qga9XDhA2MpX/4nTKwb+Eh+ey+r84hgoun 30 | ephr5hUsC9nipE/62u98MWwvRPmU7CZVzF2QBJj1PggU3l1SUM+bNdX8+zcKOu9+ 31 | 8fGEGsX77z5T6um1riCJ5cjxIYCZa78FGAuOwUFPXtCmvrtMiq5wgeOvb+wegzJm 32 | RIdnlAoLgJxVLSw/23aMYuQYMDnYsJ38GUmnzE9sI8mqT5YVr1G+r7cy3tcz4xgR 33 | iWydUoj6Fe4zfvKEYvQ7GSc7Z2e45KDT0s0t1bGn2b+wvNUcg2CT 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /http/test/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDf0qtTZ/c6yB33 3 | NbLJnPc63x9gqEIo9nVXrCwkFyv8JDa+u+K7EdtSCsJPlfOBJPWtFBxfZl8H45mg 4 | 1PgvqfNoMDwSZ5EuJsjB7EQhU6mLsOPF46h0C4RgwMMjQJfrtG0kxykJgRLXN2ba 5 | TSj4TPHXlqixeVrU5oCYagbS0Fb3U7bYzLXdj6FQWIi8Kde4oSfFGJCBXXaYrajz 6 | p+J2k6OJoQdJPHAgkrtT/TKV9z8g9HQitkxEaCw5kqNEiYmR74cDFWavwPLQr7FL 7 | 181rNQ6MBVsY+sMOek+KQf29L8yS9pLKe43sh7fmMHUnHQoYH5va63tE15VvzYUq 8 | bF82PgvOyDjtOsQqRHySEU+vXjEj3kCNXR1+/8LvO5Q0vkaOF1ZvmkOdmfg3J4bM 9 | CQ6Mz7xOkO4VK9sqKT1M8/JD0I5mQP0uQOdZBDrU3pvZKZoweF7mHls/YH7nNVO+ 10 | jeabBglYfQ8pPxI9/4rplYLqGcKUxb1kNwewegDik1GYfnvOd3CvNDMcatDbfSNe 11 | PEV0S+R7Q23ZW6HLRcvk9PBs9P1h/pguFb8NpcgXNfU50MmooxaabpemrL1MPR5M 12 | imXexStiySiaMgu9oRJjDEMpkjic9eef3I+jUp9DsFUl27K/ZRCuXHoG91rVDkrC 13 | nqDkHcdPghXJ1ScQYSk8H170xVAZuwIDAQABAoICAA83275IC19Ly/3LAOUEbyrp 14 | kNCZoCX42/GiGz449RynubEuRTRC+o6RfLS8lVnqkWm7PFKgO0XmC8QLtvRGse4b 15 | mXoG4LAqouvMjLVXYufElzgVSpbZUZ7ifGIhOjMkiWc+w2OKCyXm9m9RX7/CO49V 16 | BxNKFQr0wTmbu+sJj2AxqPUEtjjiUy2LV4Czt58uFe4oxWqGWFH5PMuUVqmJUE1D 17 | TaOFIHqWf3YueQoJTZmIBX12t6jPDHK9rawzTc9GYf+rrNe2XswhpgRqH+VyLhMH 18 | KjKAmlKZrl7QLdh3L4muijyKOhQjBjX4hX6rUsxhGc/AwxjjL53yc2N3CuI9RcAg 19 | /80hRh7T68E3CH6K2eJzIp/n68DRVB+Q5kD4Gmeo0SpzQqBiSqIQ+/UhO859ZecT 20 | 7/PbX2qd19t/MmxwGKsmxEceFiUOu8FLczSaf1Zc8F1KteESDGdoy8R1WHPYCQtc 21 | /j3IpyfrPY043ZlyFmSJt1qvKclfpFbOnQ9617rPm3uNfcpLbQx/+2s2L46VPW6/ 22 | pgH+HL5jdPSnOYX+1RR9+yibj/V0us+TEZRF16Cjd/rUnhGPyaiVQ0NqtHJ0HOWk 23 | GC0FOgTUgclYoNYE567y/JX+3AfvmDVy/wQFg55pJESPwnfleIu/+WxyBI2hLWIF 24 | 15+b+UJgj+E6MrENUdiBAoIBAQDwYNnDHDmIgo6Ut6kjLbQnKogySylHvlJAjoQ/ 25 | 8guoQ0raoOABiX2OqiQMeO0UYwifSz0gyAfyYvOvtGWlJRf/Qa7dOF0MirRAfDH+ 26 | rOeWaP2T6SIDlnOPs6GZPRTwkawz/OYvhzFfFi0NuFJqWP1ffp/638BnwZY+FOOH 27 | EsdQYgFtbH5h6QOopIz9ZaPWlP+HzZp/LaXykJ38OyvMRW94NvWrJmXehlhejCzK 28 | HTg4z/uDKXzgmBFzmnE1RT6X4XQdx6z9Q1Sv5G7yt2VA0oDnMPQ7Q6phkPQ0qZWh 29 | oXAJIJpMyxZXwx+NIY0M6Z5TykJqF8f2KefluJsBbFA+mw7DAoIBAQDuXmNgfTD2 30 | MrWnPSlti6JxZOXElD7n/iCZQGlZXizDIpUY947kjbpO+qONTmar7e2YEZ7cd3Fs 31 | fA18btY2JgRK9RHeGINFUIcnvyIDolHUPtt+xgFUx7xs6VkZm6rTAOZO9DpkSUr+ 32 | 0gJ7OE4qmIzSfQUf3P3HAOGI80eWBPgGEFXvmoHt05/tx0BgudpLQXdN93np2ta4 33 | nLUJW8qPfk5FIsuQQFpLm5St+siUAgNPlU+afPmpfXPXFQ2L1AjvR4iUhhct8af+ 34 | IL+Q6vuZsFgcwhqoEwz0uXQRPrGEi1bmIAJdaY8wzWmjyzInfWH0jwWOBCDq44PW 35 | xY2xtyq674mpAoIBAGUbd70XrCN9NtfbAKupV7N5slvlPXHN3e8Fgx8agki+NUgM 36 | juxWlpdhfVQ5sQ74MPI/xXB6cs1DTdjD+lUiaxuU1yZ5xLm3xeWx152VwLuFYdPL 37 | JeF4cRcqqXg5NwuRWmtrnq6zINMsErtIonHVCI4YRtZRMy7xQG7QfnucLHXlI1CR 38 | Z/9q8X0H6b3T+LwTZAANPUTtQHXs2mbzxJJhSme4RlE9D3Av9g+eymAhNjq/UL10 39 | v/AARqbneWK8w31C+YJlMKA+p+psHTa+aTqA0HNuJeyfo5TyUaWtByE7NFLCgeR3 40 | L+iuTCEi0Z9yzIPV+WWCpzslmUMJCTnqAz6yHd0CggEBALXDd9f93rX92RLbyplj 41 | 3szE+Yg62vUgjU8PFMY2z79EwZi2PVn+0NCkws5fuF7mIUTSOuc2lWD5/3ahNes4 42 | dhGKasOGwaK7UzR1xMQspGVKQSmcZyt02zPZhD3IvCwGBZizF0udr/g7v/Sh7MkC 43 | Zm+jMaMoasOy4kgB6tZ0l4RYNI9urCDkPJGeXHLlhZXZJ9ZmHaq7WvyPUmT+f6mH 44 | 0LtbWwp2Z+Q7kNZR410pY8jtvDO7lM0vgoPB5AB6AQ4k5/sJfOdl3IomAq3I4W5C 45 | UA4t3RHI5Ro/yEfWHJtLfUohomwEsFGwcRubOHVwYJ+R59RxRYCXWNMO9YtMduLx 46 | AXECggEAXUDzPXATwtCzwCnTQXG6trwdpNiK9ZGvd8VfUQl3ZCGtG4pIEI6d+lqM 47 | M0zEsP9UA8wJuYtbJ+++qdALmMSpUO8YCj2+VB6gpiSJosun5pdppaEDvB1bh69U 48 | 7yx7uHO+iUHLZBLduHVS67lW5sb7gwgtPEPRsuoHCAU0UtozWdueXnxxcspJlem6 49 | jQbjKH6h91EXmWbVeNM3u/AGXeuINeumzsLqX92O9Cld20aIn+4uZBZ0yzx6oYQO 50 | PguNSSYL8bgrpoA/cmOX8QOqbIs7Vven8AFCH+OXIyPUXYN3Nxi5+cJbfSc6Fyeh 51 | omQUqxvWInWrLd8PDtxPnHAlslBRVg== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /http/url.pony: -------------------------------------------------------------------------------- 1 | class val URL 2 | """ 3 | Holds the components of a URL. These are always stored as valid, URL-encoded 4 | values. 5 | """ 6 | var scheme: String = "" 7 | """ 8 | URL scheme. 9 | 10 | If the given URL does not provide a scheme, this will be the empty string. 11 | 12 | See also [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.1). 13 | """ 14 | 15 | var user: String = "" 16 | """ 17 | URL user as part of the URLs authority component: 18 | 19 | ``` 20 | authority = [ user [ ":" password ] "@" ] host [ ":" port ] 21 | ``` 22 | 23 | If the URL does not provide user information, this will be the empty string. 24 | 25 | See also [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.2.1). 26 | """ 27 | 28 | var password: String = "" 29 | """ 30 | URL password as part of the URLs authority component: 31 | 32 | ``` 33 | authority = [ user [ ":" password ] "@" ] host [ ":" port ] 34 | ``` 35 | 36 | If the URL does not provide a password, this will be the empty string. 37 | 38 | See also [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.2.1). 39 | """ 40 | 41 | var host: String = "" 42 | """ 43 | URL host as part of the URLs authority component: 44 | 45 | ``` 46 | authority = [ user [ ":" password ] "@" ] host [ ":" port ] 47 | ``` 48 | 49 | If the URL does not provide a host, this will be the empty string. 50 | 51 | See also [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.2.2). 52 | """ 53 | 54 | var port: U16 = 0 55 | """ 56 | URL port as part of the URLs authority component: 57 | 58 | ``` 59 | authority = [ user [ ":" password ] "@" ] host [ ":" port ] 60 | ``` 61 | 62 | If the URL does not provide a port, this will be the empty string. 63 | 64 | See also [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.2.3). 65 | """ 66 | 67 | var path: String = "" 68 | """ 69 | URL path component. 70 | 71 | If the URL does not provide a path component, this will be the empty string. 72 | 73 | See also [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3). 74 | """ 75 | 76 | var query: String = "" 77 | """ 78 | URL query component. 79 | 80 | If the URL does not provide a query component, this will be the empty string. 81 | 82 | See also [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.4). 83 | """ 84 | 85 | var fragment: String = "" 86 | """ 87 | Url fragment identifier component. 88 | 89 | If the URL does not provide a fragment identifier component, this will be the empty string. 90 | 91 | See also [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.5). 92 | """ 93 | 94 | new val create() => 95 | """ 96 | Create an empty URL. 97 | """ 98 | None 99 | 100 | new val build(from: String, percent_encoded: Bool = true) ? => 101 | """ 102 | Parse the URL string into its components. If it isn't URL encoded, encode 103 | it. If existing URL encoding is invalid, raise an error. 104 | """ 105 | _parse(from)? 106 | 107 | if not URLEncode.check_scheme(scheme) then error end 108 | user = URLEncode.encode(user, URLPartUser, percent_encoded)? 109 | password = URLEncode.encode(password, URLPartPassword, percent_encoded)? 110 | host = URLEncode.encode(host, URLPartHost, percent_encoded)? 111 | path = URLEncode.encode(path, URLPartPath, percent_encoded)? 112 | query = URLEncode.encode(query, URLPartQuery, percent_encoded)? 113 | fragment = URLEncode.encode(fragment, URLPartFragment, percent_encoded)? 114 | 115 | new val valid(from: String) ? => 116 | """ 117 | Parse the URL string into its components. If it isn't URL encoded, raise an 118 | error. 119 | """ 120 | _parse(from)? 121 | 122 | if not is_valid() then 123 | error 124 | end 125 | 126 | fun is_valid(): Bool => 127 | """ 128 | Return true if all elements are correctly URL encoded. 129 | """ 130 | URLEncode.check_scheme(scheme) and 131 | URLEncode.check(user, URLPartUser) and 132 | URLEncode.check(password, URLPartPassword) and 133 | URLEncode.check(host, URLPartHost) and 134 | URLEncode.check(path, URLPartPath) and 135 | URLEncode.check(query, URLPartQuery) and 136 | URLEncode.check(fragment, URLPartFragment) 137 | 138 | fun string(): String iso^ => 139 | """ 140 | Combine the components into a string. 141 | """ 142 | let len = 143 | scheme.size() + 3 + user.size() + 1 + password.size() + 1 + host.size() 144 | + 6 + path.size() + 1 + query.size() + 1 + fragment.size() 145 | let s = recover String(len) end 146 | 147 | if scheme.size() > 0 then 148 | s.append(scheme) 149 | s.append(":") 150 | end 151 | 152 | if (user.size() > 0) or (host.size() > 0) then 153 | s.append("//") 154 | end 155 | 156 | if user.size() > 0 then 157 | s.append(user) 158 | 159 | if password.size() > 0 then 160 | s.append(":") 161 | s.append(password) 162 | end 163 | 164 | s.append("@") 165 | end 166 | 167 | if host.size() > 0 then 168 | s.append(host) 169 | 170 | // Do not output port if it's the scheme default. 171 | if port != default_port() then 172 | s.append(":") 173 | s.append(port.string()) 174 | end 175 | end 176 | 177 | s.append(path) 178 | 179 | if query.size() > 0 then 180 | s.append("?") 181 | s.append(query) 182 | end 183 | 184 | if fragment.size() > 0 then 185 | s.append("#") 186 | s.append(fragment) 187 | end 188 | 189 | consume s 190 | 191 | fun val join(that: URL): URL => 192 | """ 193 | Using this as a base URL, concatenate with another, possibly relative, URL 194 | in the same way a browser does for a link. 195 | """ 196 | // TODO: 197 | this 198 | 199 | fun default_port(): U16 => 200 | """ 201 | Report the default port for our scheme. 202 | Returns 0 for unrecognised schemes. 203 | """ 204 | match scheme 205 | | "http" => 80 206 | | "https" => 443 207 | else 0 208 | end 209 | 210 | fun ref _parse(from: String) ? => 211 | """ 212 | Parse the given string as a URL. 213 | Raises an error on invalid port number. 214 | """ 215 | (var offset, scheme) = _parse_scheme(from) 216 | (offset, let authority) = _parse_part(from, "//", "/?#", offset) 217 | (offset, path) = _parse_part(from, "", "?#", offset) 218 | (offset, query) = _parse_part(from, "?", "#", offset) 219 | (offset, fragment) = _parse_part(from, "#", "", offset) 220 | 221 | if path.size() == 0 then 222 | // An empty path is a root path. 223 | path = "/" 224 | end 225 | 226 | (var userinfo, var hostport) = _split(authority, '@') 227 | 228 | if hostport.size() == 0 then 229 | // No '@' found, hostport is whole of authority. 230 | hostport = userinfo = "" 231 | end 232 | 233 | (user, password) = _split(userinfo, ':') 234 | (host, var port_str) = _parse_hostport(hostport) 235 | 236 | port = 237 | if port_str.size() > 0 then 238 | port_str.u16()? 239 | else 240 | default_port() 241 | end 242 | 243 | fun _parse_scheme(from: String): (/*offset*/ISize, /*scheme*/String) => 244 | """ 245 | Find the scheme, if any, at the start of the given string. 246 | The offset of the part following the scheme is returned. 247 | """ 248 | // We have a scheme only if we have a ':' before any of "/?#". 249 | try 250 | var i = USize(0) 251 | 252 | while i < from.size() do 253 | let c = from(i)? 254 | 255 | if c == ':' then 256 | // Scheme found. 257 | return ((i + 1).isize(), from.substring(0, i.isize())) 258 | end 259 | 260 | if (c == '/') or (c == '?') or (c == '#') then 261 | // No scheme. 262 | return (0, "") 263 | end 264 | 265 | i = i + 1 266 | end 267 | end 268 | 269 | // End of string reached without finding any relevant terminators. 270 | (0, "") 271 | 272 | fun _parse_part( 273 | from: String, 274 | prefix: String, 275 | terminators: String, 276 | offset: ISize) 277 | : (/*offset*/ISize, /*part*/String) 278 | => 279 | """ 280 | Attempt to parse the specified part out of the given string. Only attempt 281 | the parse if the given prefix is found first. Pass "" if no prefix is 282 | needed. The part ends when any one of the given terminator characters is 283 | found, or the end of the input is reached. The offset of the terminator is 284 | returned, if one is found. 285 | """ 286 | if (prefix.size() > 0) and (not from.at(prefix, offset)) then 287 | // Prefix not found. 288 | return (offset, "") 289 | end 290 | 291 | let start = offset + prefix.size().isize() 292 | 293 | try 294 | var i = start.usize() 295 | 296 | while i < from.size() do 297 | let c = from(i)? 298 | 299 | var j = USize(0) 300 | while j < terminators.size() do 301 | if terminators(j)? == c then 302 | // Terminator found. 303 | return (i.isize(), from.substring(start, i.isize())) 304 | end 305 | 306 | j = j + 1 307 | end 308 | 309 | i = i + 1 310 | end 311 | end 312 | 313 | // No terminator found, take whole string. 314 | (from.size().isize(), from.substring(start)) 315 | 316 | fun _split(src: String, separator: U8): (String, String) => 317 | """ 318 | Split the given string in 2 around the first instance of the specified 319 | separator. If the string does not contain the separator then the first 320 | resulting string is the whole src and the second is empty. 321 | """ 322 | try 323 | var i = USize(0) 324 | 325 | while i < src.size() do 326 | if src(i)? == separator then 327 | // Separator found. 328 | return (src.substring(0, i.isize()), src.substring((i + 1).isize())) 329 | end 330 | 331 | i = i + 1 332 | end 333 | end 334 | 335 | // Separator not found. 336 | (src, "") 337 | 338 | fun _parse_hostport(hostport: String): (/*host*/String, /*port*/String) => 339 | """ 340 | Split the given "host and port" string into the host and port parts. 341 | """ 342 | try 343 | if (hostport.size() == 0) or (hostport(0)? != '[') then 344 | // This is not an IPv6 format host, just split at the first ':'. 345 | return _split(hostport, ':') 346 | end 347 | 348 | // This is an IPv6 format host, need to find the ']' 349 | var i = USize(0) 350 | var terminator = U8(']') 351 | 352 | while i < hostport.size() do 353 | if hostport(i)? == terminator then 354 | if terminator == ':' then 355 | // ':' found, now we can separate the host and port 356 | return (hostport.substring(0, i.isize()), 357 | hostport.substring((i + 1).isize())) 358 | end 359 | 360 | // ']' found, now find ':' 361 | terminator = ':' 362 | end 363 | 364 | i = i + 1 365 | end 366 | end 367 | 368 | // ':' not found, we have no port. 369 | (hostport, "") 370 | -------------------------------------------------------------------------------- /http/url_encode.pony: -------------------------------------------------------------------------------- 1 | primitive URLPartUser 2 | primitive URLPartPassword 3 | primitive URLPartHost 4 | primitive URLPartPath 5 | primitive URLPartQuery 6 | primitive URLPartFragment 7 | 8 | type URLPart is 9 | ( URLPartUser 10 | | URLPartPassword 11 | | URLPartHost 12 | | URLPartPath 13 | | URLPartQuery 14 | | URLPartFragment 15 | ) 16 | 17 | 18 | primitive URLEncode 19 | """ 20 | Functions for checking, encoding, and decoding parts of URLs. 21 | """ 22 | 23 | fun encode(from: String, part: URLPart, percent_encoded: Bool = true) 24 | : String ? 25 | => 26 | """ 27 | URL encode and normilase the given string. 28 | The percent_encoded parameter indicates how '%' characters should be 29 | interpretted. 30 | true => given string is already at least partially encoded, so '%'s 31 | indicate an encoded character. 32 | false => given string is not yet encoded at all, so '%'s are just '%'s. 33 | An error is raised on invalid existing encoding or illegal characters that 34 | cannot be encoded. 35 | """ 36 | if _is_host_ipv6(from, part)? then 37 | return from 38 | end 39 | 40 | let out = recover String(from.size()) end 41 | var i = USize(0) 42 | 43 | while i < from.size() do 44 | var c = from(i)? 45 | var should_encode = false 46 | 47 | if (c == '%') and percent_encoded then 48 | // Treat % as an encoded character. 49 | // _unhex() will throw on bad / missing hex digit. 50 | c = (_unhex(from(i + 1)?)? << 4) or _unhex(from(i + 2)?)? 51 | should_encode = not _normal_decode(c, part) 52 | i = i + 3 53 | else 54 | // Not an encoded character. 55 | should_encode = not _is_char_legal(c, part) 56 | i = i + 1 57 | end 58 | 59 | if should_encode then 60 | out.push('%') 61 | out.push(_hex(c >> 4)?) 62 | out.push(_hex(c and 0xF)?) 63 | else 64 | out.push(c) 65 | end 66 | end 67 | 68 | out 69 | 70 | fun decode(from: String): String ? => 71 | """ 72 | URL decode a string. Raise an error on invalid URL encoded. 73 | """ 74 | let out = recover String(from.size()) end 75 | var i = USize(0) 76 | 77 | while i < from.size() do 78 | let c = from(i)? 79 | 80 | if c == '%' then 81 | // _unhex() will throw on bad / missing hex digit. 82 | let value = (_unhex(from(i + 1)?)? << 4) or _unhex(from(i + 2)?)? 83 | out.push(value) 84 | i = i + 3 85 | elseif c == '+' then 86 | out.push(' ') 87 | i = i + 1 88 | else 89 | out.push(c) 90 | i = i + 1 91 | end 92 | end 93 | 94 | out 95 | 96 | fun check_scheme(scheme: String): Bool => 97 | """ 98 | Check that the given string is a valid scheme. 99 | """ 100 | try 101 | var i = USize(0) 102 | 103 | while i < scheme.size() do 104 | let c = scheme(i)? 105 | 106 | if 107 | ((c < 'a') or (c > 'z')) 108 | and ((c < 'A') or (c > 'Z')) 109 | and ((c < '0') or (c > '9')) 110 | and (c != '-') 111 | and (c != '+') 112 | and (c != '.') 113 | then 114 | return false 115 | end 116 | 117 | i = i + 1 118 | end 119 | end 120 | 121 | true 122 | 123 | fun check(from: String, part: URLPart): Bool => 124 | """ 125 | Check that the given string is valid to be the given URL part without 126 | further encoding. Canonical form is not checked for, merely validity. 127 | """ 128 | try 129 | if _is_host_ipv6(from, part)? then 130 | return true 131 | end 132 | else 133 | return false 134 | end 135 | 136 | try 137 | var i = USize(0) 138 | 139 | while i < from.size() do 140 | let c = from(i)? 141 | 142 | if c == '%' then 143 | // Character is encoded. 144 | // _unhex() will throw on bad / missing hex digit. 145 | _unhex(from(i + 1)?)? 146 | _unhex(from(i + 2)?)? 147 | i = i + 3 148 | elseif _is_char_legal(c, part) then 149 | i = i + 1 150 | else 151 | return false 152 | end 153 | end 154 | true 155 | else 156 | false 157 | end 158 | 159 | fun _is_char_legal(value: U8, part: URLPart): Bool => 160 | """ 161 | Determine whether the given character is legal to appear in the specified 162 | URL part. 163 | """ 164 | // The unreserved and sub-delim characters are always allowed. 165 | if ((value >= 'a') and (value <= 'z')) or 166 | ((value >= 'A') and (value <= 'Z')) or 167 | ((value >= '0') and (value <= '9')) or 168 | (value == '-') or (value == '.') or (value == '_') or (value == '~') or 169 | (value == '!') or (value == '$') or (value == '&') or (value == '\'') or 170 | (value == '(') or (value == ')') or (value == '*') or (value == '+') or 171 | (value == ',') or (value == ';') or (value == '=') then 172 | return true 173 | end 174 | 175 | // Which general delims are allowed depends on the part. 176 | match part 177 | | URLPartPassword => (value == ':') 178 | | URLPartPath => (value == ':') or (value == '@') or (value == '/') 179 | | URLPartQuery => 180 | (value == ':') or (value == '@') or (value == '/') or (value == '?') 181 | | URLPartFragment => 182 | (value == ':') or (value == '@') or (value == '/') or (value == '?') 183 | else 184 | false 185 | end 186 | 187 | fun _normal_decode(value: U8, part: URLPart): Bool => 188 | """ 189 | Determine whether the given character should be decoded to give normal 190 | form. Some characters, such as sub-delims, are valid to have either in 191 | encoded or unencoded form. These should be left as they are when 192 | normalising. This will return false for such characters. 193 | """ 194 | // The unreserved characters should always be decoded. 195 | if 196 | ((value >= 'a') and (value <= 'z')) 197 | or ((value >= 'A') and (value <= 'Z')) 198 | or ((value >= '0') and (value <= '9')) 199 | or (value == '-') 200 | or (value == '_') 201 | or (value == '.') 202 | or (value == '~') 203 | then 204 | return true 205 | end 206 | 207 | // Which general delims to decode depends on the part. 208 | match part 209 | | URLPartPassword => (value == ':') 210 | | URLPartPath => (value == ':') or (value == '@') or (value == '/') 211 | | URLPartQuery => 212 | (value == ':') or (value == '@') or (value == '/') or (value == '?') 213 | | URLPartFragment => 214 | (value == ':') or (value == '@') or (value == '/') or (value == '?') 215 | else 216 | false 217 | end 218 | 219 | fun _is_host_ipv6(host: String, part: URLPart): Bool ? => 220 | """ 221 | Check whether the given string is a valid IPv6 format host. 222 | Returns: 223 | true if string is a valid IPv6 format host. 224 | false if string is not an IPv6 foramt host at all. 225 | Raises an error if string is an invalid IPv6 format host. 226 | """ 227 | try 228 | if (part isnt URLPartHost) or (host.size() == 0) or (host(0)? != '[') then 229 | return false 230 | end 231 | end 232 | 233 | // We are an IPv6 format host, ie a host starting with a '['. 234 | var i = USize(1) 235 | 236 | while i < (host.size() - 1) do 237 | let c = host(i)? 238 | 239 | // Only hex digits, ':' and '.' and allowed. 240 | if 241 | ((c < 'a') or (c > 'f')) 242 | and ((c < 'A') or (c > 'F')) 243 | and ((c < '0') or (c > '9')) 244 | and (c != ':') 245 | and (c != '.') 246 | then 247 | error 248 | end 249 | 250 | i = i + 1 251 | end 252 | 253 | // Must end with a ']'. 254 | if host(host.size() - 1)? != ']' then error end 255 | true 256 | 257 | fun _hex(value: U8): U8 ? => 258 | """ 259 | Turn 4 bits into a hex value. 260 | """ 261 | if value < 10 then 262 | value + '0' 263 | elseif value < 16 then 264 | (value + 'A') - 10 265 | else 266 | error 267 | end 268 | 269 | fun _unhex(value: U8): U8 ? => 270 | """ 271 | Turn a hex value into 4 bits. 272 | """ 273 | if (value >= '0') and (value <= '9') then 274 | value - '0' 275 | elseif (value >= 'A') and (value <= 'F') then 276 | (value - 'A') + 10 277 | elseif (value >= 'a') and (value <= 'f') then 278 | (value - 'a') + 10 279 | else 280 | error 281 | end 282 | -------------------------------------------------------------------------------- /make.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [Parameter(Position=0, HelpMessage="The action to take (build, test, install, package, clean).")] 3 | [string] 4 | $Command = 'build', 5 | 6 | [Parameter(HelpMessage="The build configuration (Release, Debug).")] 7 | [string] 8 | $Config = "Release", 9 | 10 | [Parameter(HelpMessage="The version number to set.")] 11 | [string] 12 | $Version = "", 13 | 14 | [Parameter(HelpMessage="Architecture (native, x64).")] 15 | [string] 16 | $Arch = "x86-64", 17 | 18 | [Parameter(HelpMessage="Directory to install to.")] 19 | [string] 20 | $Destdir = "build/install" 21 | ) 22 | 23 | $ErrorActionPreference = "Stop" 24 | 25 | $target = "http" # The name of the source package, and the base name of the .exe file that is built if this is a program, not a library. 26 | $testPath = "." # The path of the tests package relative to the $target directory. 27 | $isLibrary = $true 28 | 29 | $rootDir = Split-Path $script:MyInvocation.MyCommand.Path 30 | $srcDir = Join-Path -Path $rootDir -ChildPath $target 31 | 32 | if ($Config -ieq "Release") 33 | { 34 | $configFlag = "" 35 | $buildDir = Join-Path -Path $rootDir -ChildPath "build/release" 36 | } 37 | elseif ($Config -ieq "Debug") 38 | { 39 | $configFlag = "--debug" 40 | $buildDir = Join-Path -Path $rootDir -ChildPath "build/debug" 41 | } 42 | else 43 | { 44 | throw "Invalid -Config path '$Config'; must be one of (Debug, Release)." 45 | } 46 | 47 | # $libsDir = Join-Path -Path $rootDir -ChildPath "build/libs" 48 | 49 | if (($Version -eq "") -and (Test-Path -Path "$rootDir\VERSION")) 50 | { 51 | $Version = (Get-Content "$rootDir\VERSION") + "-" + (& git 'rev-parse' '--short' '--verify' 'HEAD^') 52 | } 53 | 54 | $ponyArgs = "--define openssl_0.9.0" 55 | 56 | Write-Host "Configuration: $Config" 57 | Write-Host "Version: $Version" 58 | Write-Host "Root directory: $rootDir" 59 | Write-Host "Source directory: $srcDir" 60 | Write-Host "Build directory: $buildDir" 61 | 62 | # generate pony templated files if necessary 63 | if (($Command -ne "clean") -and (Test-Path -Path "$rootDir\VERSION")) 64 | { 65 | $versionTimestamp = (Get-ChildItem -Path "$rootDir\VERSION").LastWriteTimeUtc 66 | Get-ChildItem -Path $srcDir -Include "*.pony.in" -Recurse | ForEach-Object { 67 | $templateFile = $_.FullName 68 | $ponyFile = $templateFile.Substring(0, $templateFile.Length - 3) 69 | $ponyFileTimestamp = [DateTime]::MinValue 70 | if (Test-Path $ponyFile) 71 | { 72 | $ponyFileTimestamp = (Get-ChildItem -Path $ponyFile).LastWriteTimeUtc 73 | } 74 | if (($ponyFileTimestamp -lt $versionTimestamp) -or ($ponyFileTimestamp -lt $_.LastWriteTimeUtc)) 75 | { 76 | Write-Host "$templateFile -> $ponyFile" 77 | ((Get-Content -Path $templateFile) -replace '%%VERSION%%', $Version) | Set-Content -Path $ponyFile 78 | } 79 | } 80 | } 81 | 82 | function BuildTarget 83 | { 84 | $binaryFile = Join-Path -Path $buildDir -ChildPath "$target.exe" 85 | $binaryTimestamp = [DateTime]::MinValue 86 | if (Test-Path $binaryFile) 87 | { 88 | $binaryTimestamp = (Get-ChildItem -Path $binaryFile).LastWriteTimeUtc 89 | } 90 | 91 | :buildFiles foreach ($file in (Get-ChildItem -Path $srcDir -Include "*.pony" -Recurse)) 92 | { 93 | if ($binaryTimestamp -lt $file.LastWriteTimeUtc) 94 | { 95 | Write-Host "corral fetch" 96 | $output = (corral fetch) 97 | $output | ForEach-Object { Write-Host $_ } 98 | if ($LastExitCode -ne 0) { throw "Error" } 99 | 100 | Write-Host "corral run -- ponyc $configFlag $ponyArgs --cpu `"$Arch`" --output `"$buildDir`" `"$srcDir`"" 101 | $output = (corral run -- ponyc $configFlag $ponyArgs --cpu "$Arch" --output "$buildDir" "$srcDir") 102 | $output | ForEach-Object { Write-Host $_ } 103 | if ($LastExitCode -ne 0) { throw "Error" } 104 | break buildFiles 105 | } 106 | } 107 | } 108 | 109 | function BuildTest 110 | { 111 | $testTarget = "test.exe" 112 | if ($testPath -eq ".") 113 | { 114 | $testTarget = "$target.exe" 115 | } 116 | 117 | $testFile = Join-Path -Path $buildDir -ChildPath $testTarget 118 | $testTimestamp = [DateTime]::MinValue 119 | if (Test-Path $testFile) 120 | { 121 | $testTimestamp = (Get-ChildItem -Path $testFile).LastWriteTimeUtc 122 | } 123 | 124 | :testFiles foreach ($file in (Get-ChildItem -Path $srcDir -Include "*.pony" -Recurse)) 125 | { 126 | if ($testTimestamp -lt $file.LastWriteTimeUtc) 127 | { 128 | Write-Host "corral fetch" 129 | $output = (corral fetch) 130 | $output | ForEach-Object { Write-Host $_ } 131 | if ($LastExitCode -ne 0) { throw "Error" } 132 | 133 | $testDir = Join-Path -Path $srcDir -ChildPath $testPath 134 | Write-Host "corral run -- ponyc $configFlag $ponyArgs --cpu `"$Arch`" --output `"$buildDir`" `"$testDir`"" 135 | $output = (corral run -- ponyc $configFlag $ponyArgs --cpu "$Arch" --output "$buildDir" "$testDir") 136 | $output | ForEach-Object { Write-Host $_ } 137 | if ($LastExitCode -ne 0) { throw "Error" } 138 | break testFiles 139 | } 140 | } 141 | 142 | Write-Output "$testTarget.exe is built" # force function to return a list of outputs 143 | return $testFile 144 | } 145 | 146 | switch ($Command.ToLower()) 147 | { 148 | "build" 149 | { 150 | if (-not $isLibrary) 151 | { 152 | BuildTarget 153 | } 154 | else 155 | { 156 | Write-Host "$target is a library; nothing to build." 157 | } 158 | break 159 | } 160 | 161 | "test" 162 | { 163 | if (-not $isLibrary) 164 | { 165 | BuildTarget 166 | } 167 | 168 | $testFile = (BuildTest)[-1] 169 | Write-Host "$testFile" 170 | & "$testFile" --exclude=integration --sequential 171 | if ($LastExitCode -ne 0) { throw "Error" } 172 | break 173 | } 174 | 175 | "clean" 176 | { 177 | if (Test-Path "$buildDir") 178 | { 179 | Write-Host "Remove-Item -Path `"$buildDir`" -Recurse -Force" 180 | Remove-Item -Path "$buildDir" -Recurse -Force 181 | } 182 | break 183 | } 184 | 185 | "distclean" 186 | { 187 | $distDir = Join-Path -Path $rootDir -ChildPath "build" 188 | if (Test-Path $distDir) 189 | { 190 | Remove-Item -Path $distDir -Recurse -Force 191 | } 192 | Remove-Item -Path "*.lib" -Force 193 | } 194 | 195 | "install" 196 | { 197 | if (-not $isLibrary) 198 | { 199 | $binDir = Join-Path -Path $Destdir -ChildPath "bin" 200 | 201 | if (-not (Test-Path $binDir)) 202 | { 203 | mkdir "$binDir" 204 | } 205 | 206 | $binFile = Join-Path -Path $buildDir -ChildPath "$target.exe" 207 | Copy-Item -Path $binFile -Destination $binDir -Force 208 | } 209 | else 210 | { 211 | Write-Host "$target is a library; nothing to install." 212 | } 213 | break 214 | } 215 | 216 | "package" 217 | { 218 | if (-not $isLibrary) 219 | { 220 | $binDir = Join-Path -Path $Destdir -ChildPath "bin" 221 | $package = "$target-x86-64-pc-windows-msvc.zip" 222 | Write-Host "Creating $package..." 223 | 224 | Compress-Archive -Path $binDir -DestinationPath "$buildDir\..\$package" -Force 225 | } 226 | else 227 | { 228 | Write-Host "$target is a library; nothing to package." 229 | } 230 | break 231 | } 232 | 233 | default 234 | { 235 | throw "Unknown command '$Command'; must be one of (libs, build, test, install, package, clean, distclean)." 236 | } 237 | } 238 | --------------------------------------------------------------------------------