├── .github ├── FUNDING.yml ├── linters │ └── .markdown-lint.yml └── workflows │ ├── add-discuss-during-sync.yml │ ├── announce-a-release.yml │ ├── breakage-against-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.2.md ├── 0.2.3.md ├── 0.2.4.md ├── 0.3.0.md ├── 0.3.1.md ├── 0.3.2.md ├── 0.3.3.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.4.6.md ├── 0.5.0.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 ├── benchmarks └── gcp │ └── terraform │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── corral.json ├── examples ├── handle_file │ └── main.pony ├── hello_world │ └── main.pony ├── httpserver │ └── httpserver.pony ├── raw_tcp │ └── raw_tcp.pony └── sync_httpserver │ └── main.pony └── http_server ├── _ignore_ascii_case.pony ├── _pending_responses.pony ├── _server_conn_handler.pony ├── _server_connection.pony ├── _server_listener.pony ├── _test.pony ├── _test_connection_handling.pony ├── _test_headers.pony ├── _test_pipelining.pony ├── _test_request_parser.pony ├── _test_response.pony ├── _test_server_error_handling.pony ├── handler.pony ├── headers.pony ├── method.pony ├── mimetypes.pony ├── request.pony ├── request_ids.pony ├── request_parser.pony ├── response.pony ├── server.pony ├── server_config.pony ├── server_notify.pony ├── session.pony ├── status.pony ├── sync_handler.pony ├── url.pony └── url_encode.pony /.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 | jobs: 11 | announce: 12 | name: Announcements 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout main 16 | uses: actions/checkout@v4.1.1 17 | with: 18 | ref: "main" 19 | token: ${{ secrets.RELEASE_TOKEN }} 20 | - name: Release notes 21 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 22 | with: 23 | entrypoint: publish-release-notes-to-github 24 | env: 25 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 26 | - name: Zulip 27 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 28 | with: 29 | entrypoint: send-announcement-to-pony-zulip 30 | env: 31 | ZULIP_API_KEY: ${{ secrets.ZULIP_RELEASE_API_KEY }} 32 | ZULIP_EMAIL: ${{ secrets.ZULIP_RELEASE_EMAIL }} 33 | - name: Last Week in Pony 34 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 35 | with: 36 | entrypoint: add-announcement-to-last-week-in-pony 37 | env: 38 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 39 | 40 | post-announcement: 41 | name: Tasks to run after the release has been announced 42 | needs: 43 | - announce 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout main 47 | uses: actions/checkout@v4.1.1 48 | with: 49 | ref: "main" 50 | token: ${{ secrets.RELEASE_TOKEN }} 51 | - name: Rotate release notes 52 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 53 | with: 54 | entrypoint: rotate-release-notes 55 | env: 56 | GIT_USER_NAME: "Ponylang Main Bot" 57 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 58 | - name: Delete announcement trigger tag 59 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 60 | with: 61 | entrypoint: delete-announcement-tag 62 | env: 63 | GIT_USER_NAME: "Ponylang Main Bot" 64 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 65 | -------------------------------------------------------------------------------- /.github/workflows/breakage-against-ponyc-latest.yml: -------------------------------------------------------------------------------- 1 | name: ponyc update breakage test 2 | 3 | on: 4 | repository_dispatch: 5 | types: [shared-docker-linux-builders-updated] 6 | 7 | jobs: 8 | vs-ponyc-latest: 9 | name: Test against ponyc main 10 | runs-on: ubuntu-latest 11 | container: 12 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-libressl-4.0.0:latest 13 | steps: 14 | - uses: actions/checkout@v4.1.1 15 | - name: Test 16 | run: make test ssl=0.9.0 config=debug 17 | - name: Send alert on failure 18 | if: ${{ failure() }} 19 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 20 | with: 21 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 22 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 23 | organization-url: 'https://ponylang.zulipchat.com/' 24 | to: notifications 25 | type: stream 26 | topic: ${{ github.repository }} scheduled job failure 27 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 28 | -------------------------------------------------------------------------------- /.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_server/" 32 | INPUT_LIBRARY_NAME: "http_server" 33 | INPUT_DOCS_BUILD_DIR: "build/http_server-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_server-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 | jobs: 10 | superlinter: 11 | name: Lint bash, docker, markdown, and yaml 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4.1.1 15 | - name: Lint codebase 16 | uses: docker://github/super-linter:v3.8.3 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | VALIDATE_ALL_CODEBASE: true 20 | VALIDATE_BASH: true 21 | VALIDATE_DOCKERFILE: true 22 | VALIDATE_MD: true 23 | VALIDATE_YAML: true 24 | 25 | verify-changelog: 26 | name: Verify CHANGELOG is valid 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4.1.1 30 | - name: Verify CHANGELOG 31 | uses: docker://ghcr.io/ponylang/changelog-tool:release 32 | with: 33 | args: changelog-tool verify 34 | 35 | vs-ponyc-release: 36 | name: Test against recent ponyc release 37 | runs-on: ubuntu-latest 38 | container: 39 | image: ghcr.io/ponylang/shared-docker-ci-x86-64-unknown-linux-builder-with-libressl-4.0.0:release 40 | steps: 41 | - uses: actions/checkout@v4.1.1 42 | - name: Test 43 | run: make test ssl=0.9.0 config=debug 44 | -------------------------------------------------------------------------------- /.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 | jobs: 11 | # all tasks that need to be done before we add an X.Y.Z tag 12 | # should be done as a step in the pre-tagging job. 13 | # think of it like this... if when you later checkout the tag for a release, 14 | # should the change be there? if yes, do it here. 15 | pre-tagging: 16 | name: Tasks run before tagging the release 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: Update CHANGELOG.md 25 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 26 | with: 27 | entrypoint: update-changelog-for-release 28 | env: 29 | GIT_USER_NAME: "Ponylang Main Bot" 30 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 31 | - name: Update VERSION 32 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 33 | with: 34 | entrypoint: update-version-in-VERSION 35 | env: 36 | GIT_USER_NAME: "Ponylang Main Bot" 37 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 38 | - name: Update version in README 39 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 40 | with: 41 | entrypoint: update-version-in-README 42 | env: 43 | GIT_USER_NAME: "Ponylang Main Bot" 44 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 45 | - name: Update corral.json 46 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 47 | with: 48 | entrypoint: update-version-in-corral-json 49 | env: 50 | GIT_USER_NAME: "Ponylang Main Bot" 51 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 52 | 53 | # tag for release 54 | # this will kick off the next stage of the release process 55 | # no additional steps should be added to this job 56 | tag-release: 57 | name: Tag the release 58 | needs: 59 | - pre-tagging 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout main 63 | uses: actions/checkout@v4.1.1 64 | with: 65 | ref: "main" 66 | token: ${{ secrets.RELEASE_TOKEN }} 67 | - name: Trigger artefact creation 68 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 69 | with: 70 | entrypoint: trigger-artefact-creation 71 | env: 72 | GIT_USER_NAME: "Ponylang Main Bot" 73 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 74 | 75 | # all cleanup tags that should happen after tagging for release should happen 76 | # in the post-tagging job. examples of things you might do: 77 | # add a new unreleased section to a changelog 78 | # set a version back to "snapshot" 79 | # in general, post-tagging is for "going back to normal" from tasks that were 80 | # done during the pre-tagging job 81 | post-tagging: 82 | name: Tasks run after tagging the release 83 | needs: 84 | - tag-release 85 | runs-on: ubuntu-latest 86 | steps: 87 | - name: Checkout main 88 | uses: actions/checkout@v4.1.1 89 | with: 90 | ref: "main" 91 | token: ${{ secrets.RELEASE_TOKEN }} 92 | - name: Add "unreleased" section to CHANGELOG.md 93 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 94 | with: 95 | entrypoint: add-unreleased-section-to-changelog 96 | env: 97 | GIT_USER_NAME: "Ponylang Main Bot" 98 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 99 | -------------------------------------------------------------------------------- /.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_server/" 54 | INPUT_LIBRARY_NAME: "http_server" 55 | INPUT_DOCS_BUILD_DIR: "build/http_server-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_server-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.2.md: -------------------------------------------------------------------------------- 1 | ## Fix HTTP/1.0 connections not closing without keep-alive 2 | 3 | Due to a logic bug this lib was not closing HTTP/1.0 connections when the request wasnt sending a `Connection` header. This caused tools like [ab](https://httpd.apache.org/docs/2.4/programs/ab.html) to hang, as they expect the connection to close to determine when the request is fully done, unless the `-k` flag is provided. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.2.3.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponylang/http_server/98be975dfe960d018868b2202ebb634b747e9a9a/.release-notes/0.2.3.md -------------------------------------------------------------------------------- /.release-notes/0.2.4.md: -------------------------------------------------------------------------------- 1 | ## Fix missing Content-Length header 2 | 3 | Setting a content length via the `BuildableResponse` constructor didn't set the corresponding header 4 | -------------------------------------------------------------------------------- /.release-notes/0.3.0.md: -------------------------------------------------------------------------------- 1 | ## Don't export test types 2 | 3 | Prior to this change, `http_server` was exporting test related types to user packages. 4 | 5 | ## Update net_ssl dependency 6 | 7 | Updated the net_ssl dependency from 1.1.4 to 1.1.5. 8 | 9 | -------------------------------------------------------------------------------- /.release-notes/0.3.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 pony compiler. Users of this version of the library won't be impacted by the coming change. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.3.2.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_server so that it works with ponyc 0.44.0. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.3.3.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.4.0.md: -------------------------------------------------------------------------------- 1 | ## Update to work with Pony 0.47.0 2 | 3 | Pony 0.47.0 disallows interfaces having private methods. We've updated accordingly. `HTTP11RequestHandler` and `Session` are now traits instead of interfaces. If you are subtyping them by structural typing, you'll now need to use nominal typing. 4 | 5 | Previously: 6 | 7 | ```pony 8 | class MyRequestHandler 9 | ``` 10 | 11 | would become: 12 | 13 | ```pony 14 | class MyRequestHandler is Session 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /.release-notes/0.4.1.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.4.2.md: -------------------------------------------------------------------------------- 1 | ## Updated default connection heartbeat length 2 | 3 | Previously, the default connection heartbeat was incorrectly set to 1ms. We've updated it to the value it was intended to be: 1000ms. 4 | 5 | Sending a heartbeat every millisecond was excessive, and this change should improve performance slightly. 6 | 7 | -------------------------------------------------------------------------------- /.release-notes/0.4.3.md: -------------------------------------------------------------------------------- 1 | ## Add support for OpenSSL 3 2 | 3 | We've added support for working with SSL libraries that use the OpenSSL 3 API by upgrading to `ponylang/net_ssl` version 1.3.0. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.4.4.md: -------------------------------------------------------------------------------- 1 | ## Update to address JSON package removal from the standard library 2 | 3 | We've updated our dependencies to address the `json` package being removed from the Pony standard library. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.4.5.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.4.6.md: -------------------------------------------------------------------------------- 1 | ## Update ponylang/net_ssl dependency 2 | 3 | We've updated our ponylang/net_ssl dependency from version 1.3.1 to version 1.3.2. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.5.0.md: -------------------------------------------------------------------------------- 1 | ## Ensure Content-Length is set for all Responses that need it 2 | 3 | Previously responses without explicitly added Content-Length didn't add that header with a `0` value. This made some HTTP clients hang. 4 | Now all responses built with this library will have a default `Content-Length` header set, unless marked with `Transfer-Encoding: chunked` 5 | 6 | ## Added `ResponseBuilderHeaders.set_content_length(content_length: USize)` 7 | 8 | This way it is more convenient to set a content-length from a numeric value. E.g. from the size of a prepared array to be passed as HTTP body: 9 | 10 | ```pony 11 | let body = "I am a teapot" 12 | let response = 13 | Responses.builder() 14 | .set_status(StatusOK) 15 | .set_content_length(body.size()) 16 | .add_header("Content-Type", "text/plain") 17 | .finish_headers() 18 | .add_chunk(body) 19 | .build() 20 | ``` 21 | 22 | ## Added `BuildableResponse.delete_header(header_name: String)` 23 | 24 | Previously it was not possible to delete a header, once set it was permanent. No it is possible to delete a header e.g. in multi-stage processing of a HTTP response. 25 | 26 | ## `ResponseBuilderBody.add_chunk()` now takes a `ByteSeq` instead of `Array[U8] val` 27 | 28 | This allows to pass `String val` as well as `Array[U8] val` to `add_chunk`. 29 | 30 | ```pony 31 | let response = Responses.builder() 32 | .set_content_length(7) 33 | .finish_headers() 34 | .add_chunk("AWESOME") 35 | .build() 36 | ``` 37 | 38 | ## `BuildableResponse.create()` now only takes a `Status` and optionally a `Version` 39 | 40 | The logic applied to set `content_length` and `transfer_encoding` from the constructor parameters was a bit brittle, so it got removed. Use both `set_content_length(content_length: USize)` and `set_transfer_encoding(chunked: (Chunked | None))` to set them: 41 | 42 | ```pony 43 | let body = "AWESOME" 44 | let response = BuildableResponse 45 | .create(StatusOK) 46 | .set_content_length(body.size()) 47 | .set_header("Content-Type", "text/plain") 48 | ``` 49 | 50 | ## `Response.transfer_coding()` changed to `.transfer_encoding()` 51 | 52 | The wording now is now equal to the actual header name set with this method. 53 | 54 | ## `BuildableResponse.set_transfer_coding()` changed to `.set_transfer_encoding()` 55 | 56 | Following the `Response` trait. 57 | -------------------------------------------------------------------------------- /.release-notes/0.6.0.md: -------------------------------------------------------------------------------- 1 | ## Add `Session.upgrade_protocol` behaviour 2 | 3 | This can be used to upgrade the underlying TCP connection to a new incompatible protocol, like websockets. 4 | 5 | Calling this new behaviour allows this TCP connection to be upgraded to another handler, serving another protocol (e.g. [WebSocket](https://www.rfc-editor.org/rfc/rfc6455.html)). 6 | 7 | Note that this method does not send an HTTP Response with a status of 101. This needs to be done before calling this behaviour. Also, the passed in `notify` will not have its methods [accepted](https://stdlib.ponylang.io/net-TCPConnectionNotify/#connected) or [connected](https://stdlib.ponylang.io/net-TCPConnectionNotify/#connected) called, as the connection is already established. 8 | 9 | After calling this behaviour, this session and the connected Handler instance will not be called again, so it is necessary to do any required clean up right after this call. 10 | 11 | See: 12 | - [Protocol Upgrade Mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) 13 | - [Upgrade Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade) 14 | -------------------------------------------------------------------------------- /.release-notes/0.6.1.md: -------------------------------------------------------------------------------- 1 | ## Rename upgrade to upgrade_protocol 2 | 3 | To allow changing the TCP handler of a running HTTP connection, `Session` specifies a method `upgrade_protocol`, which should be implemented by the concrete classes. The implementation used by the actual HTTP server had an implementation for this feature, but the method was called `upgrade`. As `Session` provides a default implementation for `upgrade_protocol`, this wasn't caught. By renaming the implementation it's now possible to use the new feature to change the handler to a custom handler to handle for example WebSocket connections. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.6.2.md: -------------------------------------------------------------------------------- 1 | ## Update LibreSSL version used on Windows 2 | 3 | We've updated the LibreSSL version used on Windows to 3.9.1. 4 | -------------------------------------------------------------------------------- /.release-notes/next-release.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponylang/http_server/98be975dfe960d018868b2202ebb634b747e9a9a/.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] - 2024-04-20 17 | 18 | ### Changed 19 | 20 | - Update LibreSSL version used on Windows ([PR #79](https://github.com/ponylang/http_server/pull/79)) 21 | 22 | ## [0.6.1] - 2024-04-02 23 | 24 | ### Fixed 25 | 26 | - Implement the correct method in the server_connection ([PR #78](https://github.com/ponylang/http_server/pull/78)) 27 | 28 | ## [0.6.0] - 2024-03-12 29 | 30 | ### Added 31 | 32 | - Add possibility to upgrade the current session to a new TCP handler ([PR #75](https://github.com/ponylang/http_server/pull/75)) 33 | 34 | ## [0.5.0] - 2024-02-18 35 | 36 | ### Fixed 37 | 38 | - Ensure Content-Length is set for all Responses that need it ([PR #74](https://github.com/ponylang/http_server/pull/74)) 39 | 40 | ### Added 41 | 42 | - Added `ResponseBuilderHeaders.set_content_length(content_length: USize)` ([PR #74](https://github.com/ponylang/http_server/pull/74)) 43 | - Added `BuildableResponse.delete_header(header_name: String)` ([PR #74](https://github.com/ponylang/http_server/pull/74)) 44 | 45 | ### Changed 46 | 47 | - `ResponseBuilderBody.add_chunk()` now takes a `ByteSeq` instead of `Array[U8] val` ([PR #74](https://github.com/ponylang/http_server/pull/74)) 48 | - `BuildableResponse.create()` now only takes a `Status` and a `Version` ([PR #74](https://github.com/ponylang/http_server/pull/74)) 49 | - `BuildableResponse.set_transfer_coding()` changed to `.set_transfer_encoding()` ([PR #74](https://github.com/ponylang/http_server/pull/74)) 50 | - `Response.transfer_coding()` changed to `.transfer_encoding()` ([PR #74](https://github.com/ponylang/http_server/pull/74)) 51 | 52 | ## [0.4.6] - 2024-01-14 53 | 54 | ### Changed 55 | 56 | - Update to ponylang/net_ssl 1.3.2 ([PR #69](https://github.com/ponylang/http_server/pull/69)) 57 | 58 | ## [0.4.5] - 2023-04-27 59 | 60 | ### Changed 61 | 62 | - Update ponylang/net_ssl dependency ([PR #55](https://github.com/ponylang/http_server/pull/55)) 63 | 64 | ## [0.4.4] - 2023-02-14 65 | 66 | ### Changed 67 | 68 | - Update for json package removal from standard library ([PR #52](https://github.com/ponylang/http_server/pull/52)) 69 | 70 | ## [0.4.3] - 2023-01-03 71 | 72 | ### Added 73 | 74 | - Add OpenSSL 3 support ([PR #51](https://github.com/ponylang/http_server/pull/51)) 75 | 76 | ## [0.4.2] - 2022-08-26 77 | 78 | ### Fixed 79 | 80 | - Update default connection heartbeat length ([PR #47](https://github.com/ponylang/http_server/pull/47)) 81 | 82 | ## [0.4.1] - 2022-02-26 83 | 84 | ### Fixed 85 | 86 | - Update to work with Pony 0.49.0 ([PR #43](https://github.com/ponylang/http_server/pull/43)) 87 | 88 | ## [0.4.0] - 2022-02-02 89 | 90 | ### Changed 91 | 92 | - Update to work with Pony 0.47.0 ([PR #42](https://github.com/ponylang/http_server/pull/42)) 93 | 94 | ## [0.3.3] - 2022-01-16 95 | 96 | ### Fixed 97 | 98 | - Update to work with Pony 0.46.0 ([PR #39](https://github.com/ponylang/http_server/pull/39)) 99 | 100 | ## [0.3.2] - 2021-09-03 101 | 102 | ### Fixed 103 | 104 | - Update to work with ponyc 0.44.0 ([PR #31](https://github.com/ponylang/http_server/pull/31)) 105 | 106 | ## [0.3.1] - 2021-05-07 107 | 108 | ### Changed 109 | 110 | - Update to deal with changes to reference capabilities subtyping rules ([PR #30](https://github.com/ponylang/http_server/pull/30)) 111 | 112 | ## [0.3.0] - 2021-04-10 113 | 114 | ### Changed 115 | 116 | - Don't export test types ([PR #27](https://github.com/ponylang/http_server/pull/27)) 117 | - Update net_ssl dependency ([PR #29](https://github.com/ponylang/http_server/pull/29)) 118 | 119 | ## [0.2.4] - 2021-02-20 120 | 121 | ### Fixed 122 | 123 | - BuildableResponse: unify constructor and setter for content length ([PR #23](https://github.com/ponylang/http_server/pull/23)) 124 | 125 | ## [0.2.3] - 2021-02-08 126 | 127 | ## [0.2.2] - 2020-12-10 128 | 129 | ### Fixed 130 | 131 | - Fix HTTP/1.0 connections not closing without keep-alive ([PR #19](https://github.com/ponylang/http_server/pull/19)) 132 | 133 | ## [0.2.1] - 2020-05-19 134 | 135 | ### Fixed 136 | 137 | - Close Connection when application requested it with Connection: close header ([PR #14](https://github.com/ponylang/http_server/pull/14)) 138 | 139 | ## [0.2.0] - 2020-05-09 140 | 141 | ### Changed 142 | 143 | - Rename package from http/server to http_server. ([PR #6](https://github.com/ponylang/http_server/pull/6)) 144 | 145 | ## [0.1.1] - 2020-05-09 146 | 147 | ## [0.1.0] - 2020-05-09 148 | 149 | -------------------------------------------------------------------------------- /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_server? 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_server/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_server/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) 2020, 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_server 4 | GET_DEPENDENCIES_WITH := corral fetch 5 | CLEAN_DEPENDENCIES_WITH := corral clean 6 | PONYC ?= ponyc 7 | COMPILE_WITH := corral run -- $(PONYC) 8 | 9 | BUILD_DIR ?= build/$(config) 10 | SRC_DIR ?= $(PACKAGE) 11 | EXAMPLES_DIR := examples 12 | BENCH_DIR := bench 13 | tests_binary := $(BUILD_DIR)/$(PACKAGE) 14 | bench_binary := $(BUILD_DIR)/bench 15 | docs_dir := build/$(PACKAGE)-docs 16 | 17 | ifdef config 18 | ifeq (,$(filter $(config),debug release)) 19 | $(error Unknown configuration "$(config)") 20 | endif 21 | endif 22 | 23 | ifeq ($(config),release) 24 | PONYC = $(COMPILE_WITH) 25 | else 26 | PONYC = $(COMPILE_WITH) --debug 27 | endif 28 | 29 | ifeq (,$(filter $(MAKECMDGOALS),clean docs realclean TAGS)) 30 | ifeq ($(ssl), 3.0.x) 31 | SSL = -Dopenssl_3.0.x 32 | else ifeq ($(ssl), 1.1.x) 33 | SSL = -Dopenssl_1.1.x 34 | else ifeq ($(ssl), 0.9.0) 35 | SSL = -Dopenssl_0.9.0 36 | else 37 | $(error Unknown SSL version "$(ssl)". Must set using 'ssl=FOO') 38 | endif 39 | endif 40 | 41 | PONYC := $(PONYC) $(SSL) 42 | 43 | SOURCE_FILES := $(shell find $(SRC_DIR) -name *.pony) 44 | EXAMPLES := $(notdir $(shell find $(EXAMPLES_DIR)/* -type d)) 45 | EXAMPLES_SOURCE_FILES := $(shell find $(EXAMPLES_DIR) -name *.pony) 46 | EXAMPLES_BINARIES := $(addprefix $(BUILD_DIR)/,$(EXAMPLES)) 47 | BENCH_SOURCE_FILES := $(shell find $(BENCH_DIR) -name *.pony) 48 | 49 | test: unit-tests build-examples 50 | 51 | unit-tests: $(tests_binary) 52 | $^ --exclude=integration --sequential 53 | 54 | $(tests_binary): $(SOURCE_FILES) | $(BUILD_DIR) 55 | $(GET_DEPENDENCIES_WITH) 56 | $(PONYC) -o $(BUILD_DIR) $(SRC_DIR) 57 | 58 | build-examples: $(EXAMPLES_BINARIES) 59 | 60 | $(EXAMPLES_BINARIES): $(BUILD_DIR)/%: $(SOURCE_FILES) $(EXAMPLES_SOURCE_FILES) | $(BUILD_DIR) 61 | $(GET_DEPENDENCIES_WITH) 62 | $(PONYC) -o $(BUILD_DIR) $(EXAMPLES_DIR)/$* 63 | 64 | clean: 65 | $(CLEAN_DEPENDENCIES_WITH) 66 | rm -rf $(BUILD_DIR) 67 | 68 | $(docs_dir): $(SOURCE_FILES) 69 | rm -rf $(docs_dir) 70 | $(GET_DEPENDENCIES_WITH) 71 | $(PONYC) --docs-public --pass=docs --output build $(SRC_DIR) 72 | 73 | docs: $(docs_dir) 74 | 75 | $(bench_binary): $(SOURCE_FILES) $(BENCH_SOURCE_FILES) | $(BUILD_DIR) 76 | $(GET_DEPENDENCIES_WITH) 77 | $(PONYC) $(BENCH_DIR) -o $(BUILD_DIR) 78 | 79 | bench: $(bench_binary) 80 | $(bench_binary) 81 | 82 | .coverage: 83 | mkdir -p .coverage 84 | 85 | coverage: .coverage $(tests_binary) 86 | kcov --include-pattern="$(SRC_DIR)" --exclude-pattern="*/test/*.pony,*/_test.pony" .coverage $(tests_binary) 87 | 88 | TAGS: 89 | ctags --recurse=yes $(SRC_DIR) 90 | 91 | all: test 92 | 93 | $(BUILD_DIR): 94 | mkdir -p $(BUILD_DIR) 95 | 96 | .PHONY: all build-examples clean TAGS test 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http_server 2 | 3 | Pony package to build server applications for the HTTP protocol. 4 | 5 | ## Status 6 | 7 | `http_server` 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 | * Install [corral](https://github.com/ponylang/corral): 12 | * `corral add github.com/ponylang/http_server.git --version 0.6.2` 13 | * Execute `corral fetch` to fetch your dependencies. 14 | * Include this package by adding `use "http_server"` to your Pony sources. 15 | * Execute `corral run -- ponyc` to compile your application 16 | 17 | 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. 18 | 19 | ## API Documentation 20 | 21 | [https://ponylang.github.io/http_server](https://ponylang.github.io/http_server) 22 | -------------------------------------------------------------------------------- /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_server 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_server` 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_server` repo from which you will release. 12 | 13 | ```bash 14 | git clone git@github.com:ponylang/http_server.git http_server-release-clean 15 | cd http_server-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_server 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_server" 2 | use "pony_bench" 3 | use "debug" 4 | use "format" 5 | 6 | actor Main is BenchmarkList 7 | new create(env: Env) => 8 | PonyBench(env, this) 9 | 10 | fun tag benchmarks(bench: PonyBench) => 11 | bench(_SimpleGetRequestBenchmark) 12 | bench(_FormSubmissionRequestBenchmark) 13 | bench(_SplitFormSubmissionRequestBenchmark) 14 | bench(_MultipartFileUploadBenchmark) 15 | bench(_ChunkedRequestBenchmark) 16 | 17 | actor _TestSession is (Session & HTTP11RequestHandler) 18 | var _c: (AsyncBenchContinue | None) = None 19 | 20 | be set_continue(c: AsyncBenchContinue) => 21 | _c = c 22 | 23 | be _receive_start(request: Request val, request_id: RequestID) => 24 | Debug("_receive_start") 25 | 26 | be _receive_chunk(data: Array[U8] val, request_id: RequestID) => 27 | Debug("_receive_chunk") 28 | 29 | be _receive_finished(request_id: RequestID) => 30 | Debug("finish") 31 | try 32 | (_c as AsyncBenchContinue).complete() 33 | end 34 | 35 | be dispose() => 36 | Debug("dispose") 37 | 38 | be send_start(response: Response val, request_id: RequestID) => None 39 | be send_chunk(data: ByteSeq val, request_id: RequestID) => None 40 | be send_cancel(request_id: RequestID) => None 41 | be send_finished(request_id: RequestID) => None 42 | 43 | be _mute() => None 44 | 45 | be _unmute() => None 46 | 47 | class _ParseRequestBenchmark 48 | let _data: Array[String] 49 | let _session: _TestSession = _TestSession.create() 50 | let _parser: HTTP11RequestParser = HTTP11RequestParser.create(_session) 51 | 52 | new create(data: Array[String]) => 53 | _data = data 54 | 55 | fun ref apply(c: AsyncBenchContinue) ? => 56 | _session.set_continue(c) 57 | _parser.reset(true, true) 58 | let data_iter = _data.values() 59 | while data_iter.has_next() do 60 | let chunk = data_iter.next()? 61 | match _parser.parse(chunk.array()) 62 | | let err: RequestParseError => 63 | Debug("parsing failed.") 64 | if not data_iter.has_next() then 65 | c.fail() 66 | end 67 | end 68 | end 69 | 70 | class iso _SimpleGetRequestBenchmark is AsyncMicroBenchmark 71 | 72 | let data: Array[String] = [ 73 | "\r\n".join( 74 | [ 75 | "GET /get HTTP/1.1" 76 | "Host: httpbin.org" 77 | "User-Agent: curl/7.58.0" 78 | "Accept: */*" 79 | "" 80 | "" 81 | ].values()) 82 | ] 83 | 84 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 85 | 86 | fun config(): BenchConfig => BenchConfig( 87 | where max_iterations' = 100) 88 | 89 | fun name(): String => "request/simple" 90 | 91 | fun ref apply(c: AsyncBenchContinue)? => 92 | Debug("running bench") 93 | _bench.apply(c)? 94 | 95 | 96 | class iso _FormSubmissionRequestBenchmark is AsyncMicroBenchmark 97 | let data: Array[String] = [ 98 | "\r\n".join([ 99 | "POST /post HTTP/1.1" 100 | "Host: httpbin.org" 101 | "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0" 102 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 103 | "Accept-Language: en-GB,en;q=0.5" 104 | "Accept-Encoding: gzip, deflate" 105 | "Referer: http://httpbin.org/forms/post" 106 | "Content-Type: application/x-www-form-urlencoded" 107 | "Content-Length: 174" 108 | "Cookie: _gauges_unique_hour=1; _gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1" 109 | "Connection: keep-alive" 110 | "Upgrade-Insecure-Requests: 1" 111 | "" 112 | "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" 113 | ].values()) 114 | ] 115 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 116 | 117 | fun config(): BenchConfig => BenchConfig( 118 | where max_iterations' = 100) 119 | 120 | fun name(): String => "request/form-submission" 121 | 122 | fun ref apply(c: AsyncBenchContinue)? => 123 | _bench.apply(c)? 124 | 125 | class iso _SplitFormSubmissionRequestBenchmark is AsyncMicroBenchmark 126 | let data: Array[String] = [ 127 | "\r\n".join([ 128 | "POST /post HTTP/1.1" 129 | "Host: httpbin.org" 130 | "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0" 131 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 132 | "Accept-Language: en-GB,en;q=0.5" 133 | "Accept-Encoding: gzip, deflate" 134 | "Referer: http://httpbin.org/forms/post" 135 | "Content-Type: application/x-www-form-urlencoded" 136 | "Content-Length: 174" 137 | "Cookie: _gauges_unique_hour=1; _gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1" 138 | "Connection: keep-alive" 139 | "Upgrade-Insecure-Req" 140 | ].values()) 141 | "\r\n".join([ 142 | "uests: 1" 143 | "" 144 | "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" 145 | ].values()) 146 | ] 147 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 148 | 149 | fun config(): BenchConfig => BenchConfig( 150 | where max_iterations' = 100) 151 | 152 | fun name(): String => "request/form-submission/split" 153 | 154 | fun ref apply(c: AsyncBenchContinue)? => 155 | _bench.apply(c)? 156 | 157 | class iso _MultipartFileUploadBenchmark is AsyncMicroBenchmark 158 | let data: Array[String] = [ 159 | "\r\n".join([ 160 | "POST /cgi-bin/request HTTP/1.1" 161 | "Host: localhost" 162 | "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0" 163 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 164 | "Accept-Language: en-GB,en;q=0.5" 165 | "Accept-Encoding: gzip, deflate" 166 | "Connection: keep-alive" 167 | "Content-Type: multipart/form-data; boundary=abcdef123456" 168 | "Content-Length: 10001" // forcing streaming 169 | "" 170 | "--abcdef123456" 171 | "Content-Disposition: form-data; name=\"random_stuff1\"" 172 | String.from_array(recover val Array[U8].init('a', 5000) end) 173 | "Content-Disposition: form-data; name=\"random_stuff2\"" 174 | String.from_array(recover val Array[U8].init('b', 4867) end) 175 | "--abcdef123456--" 176 | ].values())] 177 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 178 | 179 | fun config(): BenchConfig => BenchConfig( 180 | where max_iterations' = 100) 181 | 182 | fun name(): String => "request/multipart-file-upload" 183 | 184 | fun ref apply(c: AsyncBenchContinue)? => 185 | _bench.apply(c)? 186 | 187 | class iso _ChunkedRequestBenchmark is AsyncMicroBenchmark 188 | let data: Array[String] = [ 189 | "\r\n".join([ 190 | "GET /get HTTP/1.1" 191 | "Host: localhost:8888" 192 | "User-Agent: curl/7.58.0" 193 | "Accept: */*" 194 | "Transfer-Encoding: chunked" 195 | "Content-Type: application/x-www-form-urlencoded" 196 | "" 197 | Format.int[U64](100 where fmt=FormatHexBare) 198 | String.from_array(recover val Array[U8].init('a', 100) end) 199 | Format.int[U64](500 where fmt=FormatHexBare) 200 | String.from_array(recover val Array[U8].init('b', 500) end) 201 | "0" 202 | "" 203 | "" 204 | ].values()) 205 | ] 206 | let _bench: _ParseRequestBenchmark = _ParseRequestBenchmark(data) 207 | 208 | fun config(): BenchConfig => BenchConfig( 209 | where max_iterations' = 100) 210 | 211 | fun name(): String => "request/chunked" 212 | 213 | fun ref apply(c: AsyncBenchContinue)? => 214 | _bench.apply(c)? 215 | 216 | -------------------------------------------------------------------------------- /benchmarks/gcp/terraform/main.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | version = "3.12.0" 3 | 4 | project = var.google_project 5 | region = var.google_region 6 | zone = var.google_zone 7 | credentials = file(var.credentials_file) 8 | } 9 | 10 | resource "google_compute_network" "vpc_network" { 11 | name = "terraform-network" 12 | } 13 | 14 | 15 | resource "google_compute_instance" "bench_server" { 16 | name = "pony-http-bench-server" 17 | machine_type = "n1-standard-8" 18 | zone = var.google_zone 19 | 20 | boot_disk { 21 | initialize_params { 22 | image = "ubuntu-os-cloud/ubuntu-minimal-1804-lts" 23 | } 24 | } 25 | 26 | network_interface { 27 | network = google_compute_network.vpc_network.name 28 | 29 | access_config { 30 | // Include this section to give the VM an external ip address 31 | } 32 | } 33 | } 34 | 35 | resource "google_compute_instance" "bench_client" { 36 | name = "pony-http-bench-client" 37 | machine_type = "n1-standard-8" 38 | zone = var.google_zone 39 | 40 | boot_disk { 41 | initialize_params { 42 | image = "ubuntu-os-cloud/ubuntu-minimal-1804-lts" 43 | } 44 | } 45 | 46 | network_interface { 47 | network = "default" 48 | 49 | access_config { 50 | // Include this section to give the VM an external ip address 51 | } 52 | } 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /benchmarks/gcp/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "server_ip" { 2 | value = google_compute_instance.bench_server.network_interface.0.access_config.0.nat_ip 3 | } 4 | 5 | output "client_ip" { 6 | value = google_compute_instance.bench_client.network_interface.0.access_config.0.nat_ip 7 | } 8 | -------------------------------------------------------------------------------- /benchmarks/gcp/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "google_project" { 2 | description = "project you wanna use." 3 | } 4 | 5 | variable "credentials_file" {} 6 | 7 | variable "google_region" { 8 | description = "google compute region" 9 | default = "europe-west3" 10 | } 11 | 12 | variable "google_zone" { 13 | description = "zone" 14 | default = "europe-west3-a" 15 | } 16 | 17 | -------------------------------------------------------------------------------- /corral.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "http_server" 4 | ], 5 | "deps": [ 6 | { 7 | "locator": "github.com/ponylang/net_ssl.git", 8 | "version": "1.3.3" 9 | }, 10 | { 11 | "locator": "github.com/ponylang/valbytes.git", 12 | "version": "0.6.2" 13 | }, 14 | { 15 | "locator": "github.com/ponylang/json.git", 16 | "version": "0.1.0" 17 | } 18 | ], 19 | "info": { 20 | "description": "Library for building HTTP server applications", 21 | "homepage": "https://github.com/ponylang/http_server", 22 | "license": "BSD-2-Clause", 23 | "documentation_url": "https://ponylang.github.io/http_server/", 24 | "version": "0.6.2", 25 | "name": "http_server" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/handle_file/main.pony: -------------------------------------------------------------------------------- 1 | use "../../http_server" 2 | 3 | use "files" 4 | use "format" 5 | use "net" 6 | 7 | actor Main 8 | """ 9 | Serve a single file over HTTP, possiblky chunked if it exceeds 4096 bytes (arbitrary choice just for this example). 10 | """ 11 | new create(env: Env) => 12 | for arg in env.args.values() do 13 | if (arg == "-h") or (arg == "--help") then 14 | _print_help(env) 15 | return 16 | end 17 | end 18 | 19 | var file = 20 | try 21 | let path = env.args(1)? 22 | FilePath(FileAuth(env.root), path) 23 | else 24 | env.err.print("Missing file argument") 25 | _print_help(env) 26 | env.exitcode(1) 27 | return 28 | end 29 | // resolve file 30 | file = try 31 | file.canonical()? 32 | else 33 | env.err.print(file.path + " does not exist or is not readable") 34 | env.exitcode(1) 35 | return 36 | end 37 | 38 | // Start the top server control actor. 39 | let server = Server( 40 | TCPListenAuth(env.root), 41 | LoggingServerNotify(env), // notify for server lifecycle events 42 | BackendMaker.create(env, file) // factory for session-based application backend 43 | where config = ServerConfig( // configuration of Server 44 | where host' = "localhost", 45 | port' = "65535", 46 | max_concurrent_connections' = 65535) 47 | ) 48 | // everything is initialized, if all goes well 49 | // the server is listening on the given port 50 | // and thus kept alive by the runtime, as long its listening socket is not 51 | // closed. 52 | 53 | fun _print_help(env: Env) => 54 | env.err.print( 55 | """ 56 | Usage: 57 | 58 | handle_file 59 | 60 | """ 61 | ) 62 | 63 | class LoggingServerNotify is ServerNotify 64 | """ 65 | Notification class that is notified about 66 | important lifecycle events for the Server 67 | """ 68 | let _env: Env 69 | 70 | new iso create(env: Env) => 71 | _env = env 72 | 73 | fun ref listening(server: Server ref) => 74 | """ 75 | Called when the Server starts listening on its host:port pair via TCP. 76 | """ 77 | try 78 | (let host, let service) = server.local_address().name()? 79 | _env.err.print("connected: " + host + ":" + service) 80 | else 81 | _env.err.print("Couldn't get local address.") 82 | _env.exitcode(1) 83 | server.dispose() 84 | end 85 | 86 | fun ref not_listening(server: Server ref) => 87 | """ 88 | Called when the Server was not able to start listening on its host:port pair via TCP. 89 | """ 90 | _env.err.print("Failed to listen.") 91 | _env.exitcode(1) 92 | 93 | fun ref closed(server: Server ref) => 94 | """ 95 | Called when the Server is closed. 96 | """ 97 | _env.err.print("Shutdown.") 98 | 99 | class BackendMaker is HandlerFactory 100 | """ 101 | Fatory to instantiate a new HTTP-session-scoped backend instance. 102 | """ 103 | let _env: Env 104 | let _file_path: FilePath 105 | 106 | new val create(env: Env, file_path: FilePath) => 107 | _env = env 108 | _file_path = file_path 109 | 110 | fun apply(session: Session): Handler^ => 111 | BackendHandler.create(_env, session, _file_path) 112 | 113 | class BackendHandler is Handler 114 | """ 115 | Backend application instance for a single HTTP session. 116 | 117 | Executed on an actor representing the HTTP Session. 118 | That means we have 1 actor per TCP Connection 119 | (to be exact it is 2 as the TCPConnection is also an actor). 120 | """ 121 | let _env: Env 122 | let _session: Session 123 | let _file_sender: FileSender 124 | 125 | var _current: (Request | None) = None 126 | 127 | new ref create(env: Env, session: Session, file_path: FilePath) => 128 | _env = env 129 | _session = session 130 | _file_sender = FileSender.create(session, file_path) 131 | 132 | fun ref apply(request: Request val, request_id: RequestID) => 133 | _current = request 134 | 135 | fun ref chunk(data: ByteSeq val, request_id: RequestID) => 136 | // ignore request body 137 | None 138 | 139 | fun ref finished(request_id: RequestID) => 140 | match _current 141 | | let request: Request => 142 | if request.method() == GET then 143 | _file_sender.send_response(request_id) 144 | else 145 | let msg = "only GET is allowed" 146 | _session.send_raw( 147 | Responses.builder().set_status(StatusMethodNotAllowed) 148 | .add_header("Content-Type", "text/plain") 149 | .set_content_length(msg.size()) 150 | .finish_headers() 151 | .add_chunk(msg) 152 | .build(), 153 | request_id 154 | ) 155 | _session.send_finished(request_id) 156 | end 157 | else 158 | let msg = "Error opening file" 159 | _session.send_raw( 160 | Responses.builder().set_status(StatusInternalServerError) 161 | .add_header("Content-Type", "text/plain") 162 | .set_content_length(msg.size()) 163 | .finish_headers() 164 | .add_chunk(msg) 165 | .build(), 166 | request_id 167 | ) 168 | _session.send_finished(request_id) 169 | end 170 | _current = None 171 | 172 | actor FileSender 173 | let _content_type: String 174 | let _file: (File | None) 175 | let _file_size: USize 176 | let _chunked: (Chunked | None) 177 | let _session: Session 178 | let _crlf: Array[U8] val 179 | let _chunk_size: USize = 8192 180 | 181 | new create(session: Session, file_path: FilePath) => 182 | _session = session 183 | _file = 184 | try 185 | OpenFile(file_path) as File 186 | end 187 | _file_size = try (_file as File).size() else 0 end 188 | _content_type = MimeTypes(file_path.path) 189 | _chunked = if _file_size > _chunk_size then Chunked else None end 190 | _crlf = recover val [as U8: '\r'; '\n'] end 191 | 192 | be send_response(request_id: RequestID) => 193 | match _chunked 194 | | Chunked => 195 | send_chunked_response(request_id) 196 | | None => 197 | send_oneshot_response(request_id) 198 | end 199 | 200 | fun ref send_chunked_response(request_id: RequestID) => 201 | let response = BuildableResponse 202 | response.set_transfer_encoding(_chunked) 203 | response.set_header("Content-Type", _content_type) 204 | _session.send_start(consume response, request_id) 205 | // move to start 206 | try 207 | (_file as File).seek_start(0) 208 | this.send_chunked_chunk(request_id) 209 | else 210 | this.send_error(request_id) 211 | end 212 | 213 | be send_chunked_chunk(request_id: RequestID) => 214 | try 215 | let file = this._file as File 216 | let file_chunk = file.read(_chunk_size) 217 | if file_chunk.size() == 0 then 218 | // send last chunk 219 | let last_chunk = (recover val Format.int[USize](0 where fmt = FormatHexBare).>append(_crlf).>append(_crlf) end).array() 220 | _session.send_chunk(last_chunk, request_id) 221 | // finish sending 222 | _session.send_finished(request_id) 223 | else 224 | // manually form a chunk 225 | let chunk_prefix = (recover val Format.int[USize](file_chunk.size() where fmt = FormatHexBare).>append(_crlf) end).array() 226 | _session.send_chunk(chunk_prefix, request_id) 227 | _session.send_chunk(consume file_chunk, request_id) 228 | _session.send_chunk(_crlf, request_id) 229 | send_chunked_chunk(request_id) 230 | end 231 | else 232 | this.send_error(request_id) 233 | end 234 | 235 | fun ref send_oneshot_response(request_id: RequestID) => 236 | let response = BuildableResponse 237 | response.set_content_length(_file_size) 238 | response.set_header("Content-Type", _content_type) 239 | _session.send_start(consume response, request_id) 240 | // move to start 241 | try 242 | (_file as File).seek_start(0) 243 | this.send_oneshot_chunk(request_id) 244 | else 245 | this.send_error(request_id) 246 | end 247 | 248 | be send_oneshot_chunk(request_id: RequestID) => 249 | try 250 | let file = this._file as File 251 | let file_chunk = file.read(_file_size) // just read as much as we can get 252 | if file_chunk.size() == 0 then 253 | _session.send_finished(request_id) 254 | else 255 | _session.send_chunk(consume file_chunk, request_id) 256 | this.send_oneshot_chunk(request_id) 257 | end 258 | else 259 | this.send_error(request_id) 260 | end 261 | 262 | be send_error(request_id: RequestID) => 263 | let msg = "Error reading from file" 264 | _session.send_raw( 265 | Responses.builder().set_status(StatusInternalServerError) 266 | .add_header("Content-Type", "text/plain") 267 | .set_content_length(msg.size()) 268 | .finish_headers() 269 | .add_chunk(msg) 270 | .build(), 271 | request_id 272 | ) 273 | _session.send_finished(request_id) 274 | -------------------------------------------------------------------------------- /examples/hello_world/main.pony: -------------------------------------------------------------------------------- 1 | use "../../http_server" 2 | use "net" 3 | use "valbytes" 4 | use "debug" 5 | 6 | actor Main 7 | """ 8 | A simple HTTP server, that responds with a simple "hello world" in the response body. 9 | """ 10 | new create(env: Env) => 11 | for arg in env.args.values() do 12 | if (arg == "-h") or (arg == "--help") then 13 | _print_help(env) 14 | return 15 | end 16 | end 17 | 18 | let port = try env.args(1)? else "9292" end 19 | let limit = try env.args(2)?.usize()? else 10000 end 20 | let host = "localhost" 21 | 22 | // Start the top server control actor. 23 | let server = Server( 24 | TCPListenAuth(env.root), 25 | LoggingServerNotify(env), // notify for server lifecycle events 26 | BackendMaker // factory for session-based application backend 27 | where config = ServerConfig( // configuration of Server 28 | where host' = host, 29 | port' = port, 30 | max_concurrent_connections' = limit) 31 | ) 32 | 33 | fun _print_help(env: Env) => 34 | env.err.print( 35 | """ 36 | Usage: 37 | 38 | hello_world [ = 9292] [ = 10000] 39 | 40 | """ 41 | ) 42 | 43 | 44 | class LoggingServerNotify is ServerNotify 45 | """ 46 | Notification class that is notified about 47 | important lifecycle events for the Server 48 | """ 49 | let _env: Env 50 | 51 | new iso create(env: Env) => 52 | _env = env 53 | 54 | fun ref listening(server: Server ref) => 55 | """ 56 | Called when the Server starts listening on its host:port pair via TCP. 57 | """ 58 | try 59 | (let host, let service) = server.local_address().name()? 60 | _env.err.print("connected: " + host + ":" + service) 61 | else 62 | _env.err.print("Couldn't get local address.") 63 | _env.exitcode(1) 64 | server.dispose() 65 | end 66 | 67 | fun ref not_listening(server: Server ref) => 68 | """ 69 | Called when the Server was not able to start listening on its host:port pair via TCP. 70 | """ 71 | _env.err.print("Failed to listen.") 72 | _env.exitcode(1) 73 | 74 | fun ref closed(server: Server ref) => 75 | """ 76 | Called when the Server is closed. 77 | """ 78 | _env.err.print("Shutdown.") 79 | 80 | class val BackendMaker 81 | 82 | let _msg: String = "hello world" 83 | let _response: ByteSeqIter = Responses.builder() 84 | .set_status(StatusOK) 85 | .add_header("Content-Type", "text/plain") 86 | .add_header("Content-Length", _msg.size().string()) 87 | .finish_headers() 88 | .add_chunk(_msg.array()) 89 | .build() 90 | 91 | fun apply(session: Session): Handler ref^ => 92 | BackendHandler(session, _response) 93 | 94 | class BackendHandler is Handler 95 | let _session: Session 96 | let _response: ByteSeqIter 97 | 98 | new ref create(session: Session, response: ByteSeqIter) => 99 | _session = session 100 | _response = response 101 | 102 | fun ref apply(request: Request val, request_id: RequestID) => 103 | _session.send_raw(_response, request_id) 104 | _session.send_finished(request_id) 105 | 106 | fun ref finished(request_id: RequestID) => None 107 | 108 | -------------------------------------------------------------------------------- /examples/httpserver/httpserver.pony: -------------------------------------------------------------------------------- 1 | use "../../http_server" 2 | use "net" 3 | use "valbytes" 4 | use "debug" 5 | 6 | actor Main 7 | """ 8 | A simple HTTP Echo server, sending back the received request in the response body. 9 | """ 10 | new create(env: Env) => 11 | for arg in env.args.values() do 12 | if (arg == "-h") or (arg == "--help") then 13 | _print_help(env) 14 | return 15 | end 16 | end 17 | 18 | let port = try env.args(1)? else "50000" end 19 | let limit = try env.args(2)?.usize()? else 100 end 20 | let host = "localhost" 21 | 22 | // Start the top server control actor. 23 | let server = Server( 24 | TCPListenAuth(env.root), 25 | LoggingServerNotify(env), // notify for server lifecycle events 26 | BackendMaker.create(env) // factory for session-based application backend 27 | where config = ServerConfig( // configuration of Server 28 | where host' = host, 29 | port' = port, 30 | max_concurrent_connections' = limit) 31 | ) 32 | // everything is initialized, if all goes well 33 | // the server is listening on the given port 34 | // and thus kept alive by the runtime, as long its listening socket is not 35 | // closed. 36 | 37 | fun _print_help(env: Env) => 38 | env.err.print( 39 | """ 40 | Usage: 41 | 42 | httpserver [ = 50000] [ = 100] 43 | 44 | """ 45 | ) 46 | 47 | 48 | class LoggingServerNotify is ServerNotify 49 | """ 50 | Notification class that is notified about 51 | important lifecycle events for the Server 52 | """ 53 | let _env: Env 54 | 55 | new iso create(env: Env) => 56 | _env = env 57 | 58 | fun ref listening(server: Server ref) => 59 | """ 60 | Called when the Server starts listening on its host:port pair via TCP. 61 | """ 62 | try 63 | (let host, let service) = server.local_address().name()? 64 | _env.err.print("connected: " + host + ":" + service) 65 | else 66 | _env.err.print("Couldn't get local address.") 67 | _env.exitcode(1) 68 | server.dispose() 69 | end 70 | 71 | fun ref not_listening(server: Server ref) => 72 | """ 73 | Called when the Server was not able to start listening on its host:port pair via TCP. 74 | """ 75 | _env.err.print("Failed to listen.") 76 | _env.exitcode(1) 77 | 78 | fun ref closed(server: Server ref) => 79 | """ 80 | Called when the Server is closed. 81 | """ 82 | _env.err.print("Shutdown.") 83 | 84 | class BackendMaker is HandlerFactory 85 | """ 86 | Fatory to instantiate a new HTTP-session-scoped backend instance. 87 | """ 88 | let _env: Env 89 | 90 | new val create(env: Env) => 91 | _env = env 92 | 93 | fun apply(session: Session): Handler^ => 94 | BackendHandler.create(_env, session) 95 | 96 | class BackendHandler is Handler 97 | """ 98 | Backend application instance for a single HTTP session. 99 | 100 | Executed on an actor representing the HTTP Session. 101 | That means we have 1 actor per TCP Connection 102 | (to be exact it is 2 as the TCPConnection is also an actor). 103 | """ 104 | let _env: Env 105 | let _session: Session 106 | 107 | var _response_builder: ResponseBuilder 108 | var _body_builder: (ResponseBuilderBody | None) = None 109 | var _sent: Bool = false 110 | var _chunked: (Chunked | None) = None 111 | 112 | new ref create(env: Env, session: Session) => 113 | _env = env 114 | _session = session 115 | _response_builder = Responses.builder() 116 | 117 | fun ref apply(request: Request val, request_id: RequestID) => 118 | """ 119 | Start processing a request. 120 | 121 | Called when request-line and all headers have been parsed. 122 | Body is not yet parsed, not even received maybe. 123 | 124 | Here we already build the start of the response body and prepare 125 | the response as far as we can. If the request has no body, we send out the 126 | response already, as we have all information we need. 127 | 128 | """ 129 | _sent = false 130 | _chunked = request.transfer_coding() 131 | 132 | // build the request-headers-array - we don't have the raw sources anymore 133 | let array: Array[U8] trn = recover trn Array[U8](128) end 134 | array.>append(request.method().repr()) 135 | .>append(" ") 136 | .>append(request.uri().string()) 137 | .>append(" ") 138 | .>append(request.version().to_bytes()) 139 | .append("\r\n") 140 | for (name, value) in request.headers() do 141 | array.>append(name) 142 | .>append(": ") 143 | .>append(value) 144 | .>append("\r\n") 145 | end 146 | array.append("\r\n") 147 | let content_length = 148 | array.size() + match request.content_length() 149 | | let s: USize => s 150 | else 151 | USize(0) 152 | end 153 | var header_builder = _response_builder 154 | .set_status(StatusOK) 155 | .add_header("Content-Type", "text/plain") 156 | .add_header("Server", "http_server.pony/0.2.1") 157 | // correctly handle HTTP/1.0 keep-alive 158 | // TODO: move this handling to ServerConnection or ResponseBuilder 159 | match (request.version(), request.header("Connection")) 160 | | (HTTP10, "Keep-Alive") => 161 | header_builder = header_builder.add_header("Connection", "Keep-Alive") 162 | end 163 | 164 | // if request is chunked, we also send the response in chunked Transfer 165 | // Encoding 166 | header_builder = 167 | match _chunked 168 | | Chunked => 169 | header_builder.set_transfer_encoding(Chunked) 170 | | None => 171 | header_builder.add_header("Content-Length", content_length.string()) 172 | end 173 | // The response builder has refcap iso, so we need to do some consume and 174 | // reassign dances here 175 | _body_builder = 176 | (consume header_builder) 177 | .finish_headers() 178 | .add_chunk(consume array) // add the request headers etc as response body here 179 | 180 | if not request.has_body() then 181 | match (_body_builder = None) 182 | | let builder: ResponseBuilderBody => 183 | // already send the response if request has no body 184 | // use optimized Session API to send out all available chunks at 185 | // once using writev on the socket 186 | _session.send_raw(builder.build(), request_id) 187 | _response_builder = builder.reset() // reset the builder for later reuse within this session 188 | _sent = true 189 | end 190 | end 191 | 192 | fun ref chunk(data: ByteSeq val, request_id: RequestID) => 193 | """ 194 | Process the next chunk of data received. 195 | 196 | If we receive any data, we append it to the builder. 197 | We send stuff later, when we know we are finished. 198 | """ 199 | match (_body_builder = None) 200 | | let builder: ResponseBuilderBody => 201 | _body_builder = builder.add_chunk( 202 | match data 203 | | let adata: Array[U8] val => adata 204 | | let s: String => s.array() 205 | end 206 | ) 207 | end 208 | 209 | fun ref finished(request_id: RequestID) => 210 | """ 211 | Called when the last chunk has been handled and the full request has been received. 212 | 213 | Here we send out the full response, if the request had a body we needed to process first (see `fun chunk` above). 214 | We call `Session.send_finished(request_id)` to let the HTTP machinery finish sending and clena up resources 215 | connected to this request. 216 | """ 217 | match (_body_builder = None) 218 | | let builder: ResponseBuilderBody => 219 | match _chunked 220 | | Chunked => 221 | builder.add_chunk(recover val Array[U8](0) end) 222 | end 223 | if not _sent then 224 | let resp = builder.build() 225 | _session.send_raw(consume resp, request_id) 226 | end 227 | _response_builder = builder.reset() 228 | end 229 | // Required call to finish request handling 230 | // if missed out, the server will misbehave 231 | _session.send_finished(request_id) 232 | 233 | -------------------------------------------------------------------------------- /examples/raw_tcp/raw_tcp.pony: -------------------------------------------------------------------------------- 1 | """ 2 | Baseline for comparing benchmark results. 3 | 4 | This file is intende ot receive body-less HTTP requests 5 | and is writing out a pre-allocated response for each request it receives. 6 | 7 | There is no actual parsing happening, other than looking for a double CRLF, 8 | marking the request end. 9 | """ 10 | use "net" 11 | use "valbytes" 12 | use "../../http_server" 13 | 14 | 15 | class MyTCPConnectionNotify is TCPConnectionNotify 16 | var buf: ByteArrays = ByteArrays() 17 | let _res: String 18 | var _handler: (ConnectionHandler | None) = None 19 | 20 | new iso create(res_str: String) => 21 | _res = res_str 22 | 23 | fun ref accepted(conn: TCPConnection ref) => 24 | let tag_conn: TCPConnection tag = conn 25 | _handler = ConnectionHandler(tag_conn, _res) 26 | 27 | 28 | fun ref received( 29 | conn: TCPConnection ref, 30 | data: Array[U8] iso, 31 | times: USize) 32 | : Bool 33 | => 34 | buf = buf + consume data 35 | var carry_on = true 36 | while carry_on do 37 | match buf.find("\r\n\r\n") 38 | | (true, let req_end: USize) => 39 | let req = buf.trim(0, req_end + 4) 40 | try 41 | let h = (_handler as ConnectionHandler) 42 | h.receive(req) 43 | end 44 | buf = buf.drop(req_end + 4) 45 | else 46 | carry_on = false 47 | end 48 | end 49 | true 50 | 51 | fun ref connect_failed(conn: TCPConnection ref) => 52 | None 53 | 54 | actor ConnectionHandler 55 | let _conn: TCPConnection tag 56 | let _res: String 57 | 58 | new create(conn: TCPConnection, res: String) => 59 | _conn = conn 60 | _res = res 61 | 62 | be receive(array: Array[U8] val) => 63 | _conn.write(_res) 64 | 65 | class MyTCPListenNotify is TCPListenNotify 66 | 67 | let res: String = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 11\r\n\r\nHELLO WORLD" 68 | 69 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ => 70 | MyTCPConnectionNotify(res) 71 | 72 | fun ref not_listening(listen: TCPListener ref) => 73 | None 74 | 75 | actor Main 76 | new create(env: Env) => 77 | try 78 | let limit = env.args(2)?.usize()? 79 | TCPListener(TCPListenAuth(env.root), 80 | recover MyTCPListenNotify end, "127.0.0.1", env.args(1)? where limit = limit) 81 | end 82 | -------------------------------------------------------------------------------- /examples/sync_httpserver/main.pony: -------------------------------------------------------------------------------- 1 | use "../../http_server" 2 | use "net" 3 | use "time" 4 | use "valbytes" 5 | 6 | actor Main 7 | """ 8 | A simple HTTP Echo server, sending back the received request in the response body. 9 | 10 | This time using a synchronous interface to the http library. 11 | """ 12 | new create(env: Env) => 13 | for arg in env.args.values() do 14 | if (arg == "-h") or (arg == "--help") then 15 | _print_help(env) 16 | return 17 | end 18 | end 19 | 20 | let port = try env.args(1)? else "50001" end 21 | let limit = try env.args(2)?.usize()? else 100 end 22 | let host = "localhost" 23 | 24 | // Start the top server control actor. 25 | let server = Server( 26 | TCPListenAuth(env.root), 27 | LoggingServerNotify(env), 28 | // HandlerFactory - used to instantiate the session-scoped Handler 29 | {(session) => 30 | SyncHandlerWrapper( 31 | session, 32 | object ref is SyncHandler 33 | 34 | 35 | var builder: (ResponseBuilder | None) = Responses.builder() 36 | """ 37 | response builder - reused within a session 38 | """ 39 | 40 | fun ref apply(request: Request val, body: (ByteArrays | None)): ByteSeqIter ? => 41 | """ 42 | Handle a new full HTTP Request including body. 43 | Return a ByteSeqIter representing the Response. 44 | 45 | This is made easy using the ResponseBuilder returned from 46 | 47 | ```pony 48 | Responses.builder() 49 | ``` 50 | 51 | This handler allows for failing, but must return a result synchronously. 52 | That means calling other actors is possible for side-effects (like e.g. logging), 53 | but the response ust be constructed when this function returns. 54 | In return the API is much simpler that the threefold cascade of receiving requests: 55 | 56 | * Handler.apply(request, request_id) 57 | * Handler.chunk(data, request_id) 58 | * Handler.finished(request_id) 59 | 60 | And the (at maximum) threefold API to send responses: 61 | 62 | * Session.send_start(response, request_id) 63 | * Session.send_chunk(data, request_id) 64 | * Session.send_finished(request_id) 65 | 66 | The API is much simpler, but the request body is aggregated into a `ByteArrays` instance, 67 | which is suboptimal for big requests and might not perform as well as the more verbose API listed above, 68 | especially for streaming contexts. 69 | """ 70 | 71 | // serialize Request for sending it back 72 | // TODO: have a good api for that on the request class itself 73 | let array: Array[U8] trn = recover trn Array[U8](128) end 74 | array.>append(request.method().repr()) 75 | .>append(" ") 76 | .>append(request.uri().string()) 77 | .>append(" ") 78 | .>append(request.version().to_bytes()) 79 | .append("\r\n") 80 | for (name, value) in request.headers() do 81 | array.>append(name) 82 | .>append(": ") 83 | .>append(value) 84 | .>append("\r\n") 85 | end 86 | array.append("\r\n") 87 | 88 | var header_builder = ((builder = None) as ResponseBuilder) 89 | .set_status(StatusOK) 90 | .set_transfer_encoding(request.transfer_coding()) 91 | .add_header("Content-Type", "text/plain") 92 | .add_header("Server", "http_server.pony/0.2.1") 93 | 94 | // correctly handle HTTP/1.0 keep alive 95 | // TODO: move this away from user responsibility 96 | // into the lib 97 | match (request.version(), request.header("Connection")) 98 | | (HTTP10, "Keep-Alive") => 99 | header_builder = header_builder.add_header("Connection", "Keep-Alive") 100 | end 101 | // add a Content-Length header if we have no chunked Transfer 102 | // Encoding 103 | match request.transfer_coding() 104 | | None => 105 | let content_length = 106 | array.size() + match request.content_length() 107 | | let s: USize => s 108 | else 109 | USize(0) 110 | end 111 | header_builder.add_header("Content-Length", content_length.string()) 112 | end 113 | let body_builder = (consume header_builder) 114 | .finish_headers() 115 | .add_chunk(consume array) // write request headers etc to response body 116 | match body 117 | | let ba: ByteArrays => 118 | // write request body to response body 119 | for chunk in ba.arrays().values() do 120 | body_builder.add_chunk(chunk) 121 | end 122 | end 123 | let res = body_builder.build() 124 | builder = (consume body_builder).reset() // enable reuse 125 | res // return the built response as ByteSeqIter synchronously 126 | end 127 | ) 128 | } 129 | where config = ServerConfig( 130 | where host' = host, 131 | port' = port, 132 | max_concurrent_connections' = limit) 133 | ) 134 | 135 | fun _print_help(env: Env) => 136 | env.err.print( 137 | """ 138 | Usage: 139 | 140 | sync_httpserver [ = 50001] [ = 100] 141 | 142 | """ 143 | ) 144 | 145 | 146 | class LoggingServerNotify is ServerNotify 147 | let _env: Env 148 | 149 | new iso create(env: Env) => 150 | _env = env 151 | 152 | fun ref listening(server: Server ref) => 153 | try 154 | (let host, let service) = server.local_address().name()? 155 | _env.err.print("connected: " + host + ":" + service) 156 | else 157 | _env.err.print("Couldn't get local address.") 158 | server.dispose() 159 | _env.exitcode(1) 160 | end 161 | 162 | fun ref not_listening(server: Server ref) => 163 | _env.err.print("Failed to listen.") 164 | _env.exitcode(1) 165 | 166 | fun ref closed(server: Server ref) => 167 | _env.err.print("Shutdown.") 168 | 169 | -------------------------------------------------------------------------------- /http_server/_ignore_ascii_case.pony: -------------------------------------------------------------------------------- 1 | primitive IgnoreAsciiCase 2 | """ 3 | Compares two strings lexicographically and case-insensitively. 4 | Only works for ASCII strings. 5 | """ 6 | 7 | fun compare(left: String, right: String): Compare => 8 | """ 9 | 10 | Less: left sorts lexicographically smaller than right 11 | Equal: same size, same content 12 | Greater: left sorts lexicographically higher than right 13 | 14 | _compare("A", "B") ==> Less 15 | _compare("AA", "A") ==> Greater 16 | _compare("A", "AA") ==> Less 17 | _compare("", "") ==> Equal 18 | """ 19 | let ls = left.size() 20 | let rs = right.size() 21 | let min = ls.min(rs) 22 | 23 | var i = USize(0) 24 | while i < min do 25 | try 26 | let lc = _lower(left(i)?) 27 | let rc = _lower(right(i)?) 28 | if lc < rc then 29 | return Less 30 | elseif rc < lc then 31 | return Greater 32 | end 33 | else 34 | Less // should not happen, size checked 35 | end 36 | i = i + 1 37 | end 38 | // all characters equal up to min size 39 | if ls > min then 40 | // left side is longer, so considered greater 41 | Greater 42 | elseif rs > min then 43 | // right side is longer, so considered greater 44 | Less 45 | else 46 | // both sides equal size and content 47 | Equal 48 | end 49 | 50 | fun eq(left: String, right: String): Bool => 51 | """ 52 | Returns true if both strings have the same size 53 | and compare equal ignoring ASCII casing. 54 | """ 55 | if left.size() != right.size() then 56 | false 57 | else 58 | var i: USize = 0 59 | while i < left.size() do 60 | try 61 | if _lower(left(i)?) != _lower(right(i)?) then 62 | return false 63 | end 64 | else 65 | return false 66 | end 67 | i = i + 1 68 | end 69 | true 70 | end 71 | 72 | fun _lower(c: U8): U8 => 73 | if (c >= 0x41) and (c <= 0x5A) then 74 | c + 0x20 75 | else 76 | c 77 | end 78 | 79 | 80 | -------------------------------------------------------------------------------- /http_server/_pending_responses.pony: -------------------------------------------------------------------------------- 1 | use "collections/persistent" 2 | use "itertools" 3 | 4 | class val _ByteSeqsWrapper is ByteSeqIter 5 | var byteseqs: Vec[_ByteSeqs] 6 | 7 | new val create(bs: Vec[_ByteSeqs]) => 8 | byteseqs = bs 9 | 10 | fun values(): Iterator[ByteSeq] ref^ => 11 | Iter[_ByteSeqs](byteseqs.values()) 12 | .flat_map[ByteSeq]( 13 | {(bs) => 14 | match bs 15 | | let b: ByteSeq => 16 | object ref is Iterator[ByteSeq] 17 | var returned: Bool = false 18 | fun has_next(): Bool => 19 | not returned 20 | fun next(): ByteSeq => 21 | b 22 | end 23 | | let bsi: ByteSeqIter => bsi.values() 24 | end 25 | }) 26 | 27 | type _ByteSeqs is (ByteSeq | ByteSeqIter) 28 | type _PendingResponse is (RequestID, Vec[_ByteSeqs]) 29 | 30 | class ref _PendingResponses 31 | // TODO: find out what is the most efficient way to 32 | // keep and accumulate a pending response 33 | // from ByteSeq and ByteSeqIter 34 | embed _pending: Array[_PendingResponse] ref = _pending.create(0) 35 | 36 | new ref create() => None // forcing ref refcap 37 | 38 | fun ref add_pending(request_id: RequestID, response_data: Array[U8] val) => 39 | // - insort by request_id, descending, so that when we pop, we don't need to 40 | // move the other entries, only when we receive entries with higher request-id 41 | try 42 | var i = USize(0) 43 | var l = USize(0) 44 | var r = _pending.size() 45 | while l < r do 46 | i = (l + r).fld(2) 47 | let entry = _pending(i)? 48 | match entry._1.compare(request_id) 49 | | Greater => 50 | l = i + 1 51 | | Equal => 52 | // already there, ignore 53 | // TODO: we should error here 54 | return 55 | else 56 | r = i 57 | end 58 | end 59 | _pending.insert( 60 | l, 61 | ( 62 | request_id, 63 | Vec[_ByteSeqs].push(response_data) 64 | ) 65 | )? 66 | end 67 | 68 | fun ref add_pending_arrays(request_id: RequestID, data: ByteSeqIter) => 69 | try 70 | var i = USize(0) 71 | var l = USize(0) 72 | var r = _pending.size() 73 | while l < r do 74 | i = (l + r).fld(2) 75 | let entry = _pending(i)? 76 | match entry._1.compare(request_id) 77 | | Greater => 78 | l = i + 1 79 | | Equal => 80 | // already there, ignore 81 | // TODO: we should error here 82 | return 83 | else 84 | r = i 85 | end 86 | end 87 | _pending.insert(l, (request_id, Vec[_ByteSeqs].push(data)))? 88 | end 89 | 90 | fun ref append_data(request_id: RequestID, data: ByteSeq val) => 91 | try 92 | var i = USize(0) 93 | var l = USize(0) 94 | var r = _pending.size() 95 | while l < r do 96 | i = (l + r).fld(2) 97 | let entry = _pending(i)? 98 | match entry._1.compare(request_id) 99 | | Greater => 100 | l = i + 1 101 | | Equal => 102 | _pending(i)? = (entry._1, entry._2.push(data)) 103 | return 104 | else 105 | r = i 106 | end 107 | end 108 | end 109 | 110 | 111 | fun ref pop(request_id: RequestID): ((RequestID, ByteSeqIter) | None) => 112 | try 113 | let last_i = _pending.size() - 1 114 | let entry = _pending(last_i)? 115 | if entry._1 == request_id then 116 | (let id, let byteseqs) = _pending.delete(last_i)? 117 | (id, _ByteSeqsWrapper(byteseqs)) 118 | end 119 | end 120 | 121 | fun has_pending(): Bool => size() > 0 122 | fun size(): USize => _pending.size() 123 | 124 | fun debug(): String => 125 | let ps = _pending.size() * 3 126 | let s = recover trn String(ps) end 127 | for k in _pending.values() do 128 | s.>append(k._1.string()).append(", ") 129 | end 130 | consume s 131 | -------------------------------------------------------------------------------- /http_server/_server_conn_handler.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "debug" 3 | 4 | class _ServerConnHandler is TCPConnectionNotify 5 | """ 6 | This is the network notification handler for the server. 7 | It handles I/O for a single HTTP connection (which includes at least 8 | one HTTP request, and possibly more with Keep-Alive). It parses HTTP 9 | requests and forwards them to the session it is bound to. 10 | Functions in this class execute within the `TCPConnection` actor. 11 | """ 12 | let _handlermaker: HandlerFactory val 13 | let _registry: _SessionRegistry tag 14 | let _config: ServerConfig 15 | 16 | var _parser: (HTTP11RequestParser | None) = None 17 | var _session: (_ServerConnection | None) = None 18 | 19 | new iso create( 20 | handlermaker: HandlerFactory val, 21 | registry: _SessionRegistry, 22 | config: ServerConfig) 23 | => 24 | """ 25 | Initialize the context for parsing incoming HTTP requests. 26 | """ 27 | _handlermaker = handlermaker 28 | _registry = registry 29 | _config = config 30 | 31 | fun ref accepted(conn: TCPConnection ref) => 32 | """ 33 | Accept the incoming TCP connection and create the actor that will 34 | manage further communication, and the message parser that feeds it. 35 | """ 36 | let sconn = _ServerConnection(_handlermaker, _config, conn) 37 | _registry.register_session(sconn) 38 | _session = sconn 39 | _parser = HTTP11RequestParser.create(sconn) 40 | 41 | fun ref received( 42 | conn: TCPConnection ref, 43 | data: Array[U8] iso, 44 | times: USize) 45 | : Bool 46 | => 47 | """ 48 | Pass chunks of data to the `HTTP11RequestParser` for this session. It will 49 | then pass completed information on the `Session`. 50 | """ 51 | // TODO: inactivity timer 52 | // add a "reset" API to Timers 53 | 54 | match _parser 55 | | let b: HTTP11RequestParser => 56 | // Let the parser take a look at what has been received. 57 | let res = b.parse(consume data) 58 | match res 59 | // Any syntax errors will terminate the connection. 60 | | let rpe: RequestParseError => 61 | Debug("Parser: RPE") 62 | conn.close() 63 | | NeedMore => 64 | Debug("Parser: NeedMore") 65 | end 66 | end 67 | true 68 | 69 | fun ref throttled(conn: TCPConnection ref) => 70 | """ 71 | Notification that the TCP connection to the client can not accept data 72 | for a while. 73 | """ 74 | try 75 | (_session as _ServerConnection).throttled() 76 | end 77 | 78 | fun ref unthrottled(conn: TCPConnection ref) => 79 | """ 80 | Notification that the TCP connection can resume accepting data. 81 | """ 82 | try 83 | (_session as _ServerConnection).unthrottled() 84 | end 85 | 86 | fun ref closed(conn: TCPConnection ref) => 87 | """ 88 | The connection has been closed. Abort the session. 89 | """ 90 | try 91 | let sconn = (_session as _ServerConnection) 92 | _registry.unregister_session(sconn) 93 | sconn.closed() 94 | end 95 | 96 | fun ref connect_failed(conn: TCPConnection ref) => 97 | None 98 | 99 | -------------------------------------------------------------------------------- /http_server/_server_connection.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "collections" 3 | use "valbytes" 4 | use "debug" 5 | use "time" 6 | 7 | actor _ServerConnection is (Session & HTTP11RequestHandler) 8 | """ 9 | Manages a stream of requests coming into a server from a single client, 10 | dispatches those request to a back-end, and returns the responses back 11 | to the client. 12 | 13 | TODO: how to handle 101 Upgrade - set new notify for the connection 14 | """ 15 | let _backend: Handler 16 | let _config: ServerConfig 17 | let _conn: TCPConnection 18 | var _close_after: (RequestID | None) = None 19 | 20 | var _active_request: RequestID = RequestIDs.max_value() 21 | """ 22 | keeps the request_id of the request currently active. 23 | That is, that has been sent to the backend last. 24 | """ 25 | var _sent_response: RequestID = RequestIDs.max_value() 26 | """ 27 | Keeps track of the request_id for which we sent a response already 28 | in order to determine lag in request handling. 29 | """ 30 | let _pending_responses: _PendingResponses = _PendingResponses.create() 31 | 32 | var _last_activity_ts: I64 = Time.seconds() 33 | 34 | new create( 35 | handlermaker: HandlerFactory val, 36 | config: ServerConfig, 37 | conn: TCPConnection) 38 | => 39 | """ 40 | Create a connection actor to manage communication with to a new 41 | client. We also create an instance of the application's back-end 42 | handler that will process incoming requests. 43 | """ 44 | _backend = handlermaker(this) 45 | _config = config 46 | _conn = conn 47 | 48 | 49 | fun ref _reset_timeout() => 50 | _last_activity_ts = Time.seconds() 51 | 52 | be _receive_start(request: Request val, request_id: RequestID) => 53 | _reset_timeout() 54 | _active_request = request_id 55 | // detemine if we need to close the connection after this request 56 | match (request.version(), request.header("Connection")) 57 | | (HTTP11, "close") => 58 | _close_after = request_id 59 | | (HTTP10, let connection_header: String) if connection_header != "Keep-Alive" => 60 | _close_after = request_id 61 | | (HTTP10, None) => 62 | _close_after = request_id 63 | end 64 | _backend(request, request_id) 65 | if _pending_responses.size() >= _config.max_request_handling_lag then 66 | // Backpressure incoming requests if the queue grows too much. 67 | // The backpressure prevents filling up memory with queued 68 | // requests in the case of a runaway client. 69 | _conn.mute() 70 | end 71 | 72 | be _receive_chunk(data: ByteSeq val, request_id: RequestID) => 73 | """ 74 | Receive some `request` body data, which we pass on to the handler. 75 | """ 76 | _reset_timeout() 77 | _backend.chunk(data, request_id) 78 | 79 | be _receive_finished(request_id: RequestID) => 80 | """ 81 | Indicates that the last *inbound* body chunk has been sent to 82 | `_chunk`. This is passed on to the back end. 83 | """ 84 | _backend.finished(request_id) 85 | 86 | be _receive_failed(parse_error: RequestParseError, request_id: RequestID) => 87 | _backend.failed(parse_error, request_id) 88 | // TODO: close the connection? 89 | 90 | be dispose() => 91 | """ 92 | Close the connection from the server end. 93 | """ 94 | _conn.dispose() 95 | 96 | be closed() => 97 | _backend.closed() 98 | _conn.unmute() 99 | 100 | //// SEND RESPONSE API //// 101 | //// STANDARD API 102 | 103 | be send_start(response: Response val, request_id: RequestID) => 104 | """ 105 | Initiate transmission of the HTTP Response message for the current 106 | Request. 107 | """ 108 | _send_start(response, request_id) 109 | 110 | 111 | fun ref _send_start(response: Response val, request_id: RequestID) => 112 | _conn.unmute() 113 | 114 | // honor Connection: close header set by application 115 | match response.header("Connection") 116 | | "close" => _close_after = request_id 117 | end 118 | 119 | let expected_id = RequestIDs.next(_sent_response) 120 | if request_id == expected_id then 121 | // just send it through. all good 122 | _sent_response = request_id 123 | _send(response) 124 | elseif RequestIDs.gt(request_id, expected_id) then 125 | // add serialized response to pending requests 126 | _pending_responses.add_pending(request_id, response.array()) 127 | else 128 | // request_id < _active_request 129 | // latecomer - ignore 130 | None 131 | end 132 | 133 | fun ref _send(response: Response val) => 134 | """ 135 | Send a single response to the underlying TCPConnection. 136 | """ 137 | _reset_timeout() 138 | _conn.write(response.array()) 139 | 140 | be send_chunk(data: ByteSeq val, request_id: RequestID) => 141 | """ 142 | Write low level outbound raw byte stream. 143 | """ 144 | if request_id == _sent_response then 145 | _reset_timeout() 146 | _conn.write(data) 147 | elseif RequestIDs.gt(request_id, _active_request) then 148 | _pending_responses.append_data(request_id, data) 149 | else 150 | None // latecomer, ignore 151 | end 152 | 153 | be send_finished(request_id: RequestID) => 154 | """ 155 | We are done sending a response. We close the connection if 156 | `keepalive` was not requested. 157 | """ 158 | _send_finished(request_id) 159 | 160 | 161 | fun ref _send_finished(request_id: RequestID) => 162 | // check if the next request_id is already in the pending list 163 | // if so, write it 164 | var rid = request_id 165 | while _pending_responses.has_pending() do 166 | match _pending_responses.pop(RequestIDs.next(rid)) 167 | | (let next_rid: RequestID, let response_data: ByteSeqIter) => 168 | //Debug("also sending next response for request: " + next_rid.string()) 169 | rid = next_rid 170 | _sent_response = next_rid 171 | _reset_timeout() 172 | _conn.writev(response_data) 173 | else 174 | // next one not available yet 175 | break 176 | end 177 | end 178 | 179 | match _close_after 180 | | let close_after_me: RequestID if RequestIDs.gte(request_id, close_after_me) => 181 | // only close after a request that requested it 182 | // in case of pipelining, we might receive a response for another, later 183 | // request earlier and would close prematurely. 184 | _conn.dispose() 185 | end 186 | 187 | be send_cancel(request_id: RequestID) => 188 | """ 189 | Cancel the current response. 190 | 191 | TODO: keep this??? 192 | """ 193 | _cancel(request_id) 194 | 195 | fun ref _cancel(request_id: RequestID) => 196 | if (_active_request - _sent_response) != 0 then 197 | // we still have some stuff in flight at the backend 198 | _backend.cancelled(request_id) 199 | end 200 | 201 | //// CONVENIENCE API 202 | 203 | be send_no_body(response: Response val, request_id: RequestID) => 204 | """ 205 | Start and finish sending a response without a body. 206 | 207 | This function calls `send_finished` for you, so no need to call it yourself. 208 | """ 209 | _send_start(response, request_id) 210 | _send_finished(request_id) 211 | 212 | be send(response: Response val, body: ByteArrays, request_id: RequestID) => 213 | """ 214 | Start and finish sending a response with body. 215 | """ 216 | _send_start(response, request_id) 217 | if request_id == _sent_response then 218 | _reset_timeout() 219 | _conn.writev(body.arrays()) 220 | _send_finished(request_id) 221 | elseif RequestIDs.gt(request_id, _active_request) then 222 | 223 | _pending_responses.add_pending_arrays( 224 | request_id, 225 | body.arrays().>unshift(response.array()) 226 | ) 227 | else 228 | None // latecomer, ignore 229 | end 230 | 231 | //// OPTIMIZED API 232 | 233 | be send_raw(raw: ByteSeqIter, request_id: RequestID, close_session: Bool = false) => 234 | """ 235 | If you have your response already in bytes, and don't want to build an expensive 236 | [Response](http_server-Response) object, use this method to send your [ByteSeqIter](builtin-ByteSeqIter). 237 | This `raw` argument can contain only the response without body, 238 | in which case you can send the body chunks later on using `send_chunk`, 239 | or, to further optimize your writes to the network, it might already contain 240 | the response body. 241 | 242 | If the session should be closed after sending this response, 243 | no matter the requested standard HTTP connection handling, 244 | set `close_session` to `true`. To be a good HTTP citizen, include 245 | a `Connection: close` header in the raw response, to signal to the client 246 | to also close the session. 247 | If set to `false`, then normal HTTP connection handling applies 248 | (request `Connection` header, HTTP/1.0 without `Connection: keep-alive`, etc.). 249 | 250 | In each case, finish sending your raw response using `send_finished`. 251 | """ 252 | _conn.unmute() 253 | if close_session then 254 | // session will be closed when calling send_finished() 255 | _close_after = request_id 256 | end 257 | let expected_id = RequestIDs.next(_sent_response) 258 | if request_id == expected_id then 259 | _sent_response = request_id 260 | _reset_timeout() 261 | _conn.writev(raw) 262 | elseif RequestIDs.gt(request_id, expected_id) then 263 | //Debug("enqueing " + request_id.string() + ". Expected " + expected_id.string()) 264 | _pending_responses.add_pending_arrays(request_id, raw) 265 | end 266 | 267 | be throttled() => 268 | """ 269 | TCP connection can not accept data for a while. 270 | """ 271 | _backend.throttled() 272 | 273 | be unthrottled() => 274 | """ 275 | TCP connection can not accept data for a while. 276 | """ 277 | _backend.unthrottled() 278 | 279 | be _mute() => 280 | _conn.mute() 281 | 282 | be _unmute() => 283 | _conn.unmute() 284 | 285 | be _heartbeat(current_seconds: I64) => 286 | let timeout = _config.connection_timeout.i64() 287 | //Debug("current_seconds=" + current_seconds.string() + ", last_activity=" + _last_activity_ts.string()) 288 | if (timeout > 0) and ((current_seconds - _last_activity_ts) >= timeout) then 289 | //Debug("Connection timed out.") 290 | // backend is notified asynchronously when the close happened 291 | dispose() 292 | end 293 | 294 | be upgrade_protocol(notify: TCPConnectionNotify iso) => 295 | _conn.set_notify(consume notify) 296 | 297 | -------------------------------------------------------------------------------- /http_server/_server_listener.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "net_ssl" 3 | 4 | class _ServerListener is TCPListenNotify 5 | """ 6 | Manages the listening socket for an HTTP server. Incoming requests 7 | are assembled and dispatched. 8 | """ 9 | let _server: Server 10 | let _config: ServerConfig 11 | let _sslctx: (SSLContext | None) 12 | let _handlermaker: HandlerFactory val 13 | 14 | new iso create( 15 | server: Server, 16 | config: ServerConfig, 17 | sslctx: (SSLContext | None), 18 | handler: HandlerFactory val) // Makes a unique session handler 19 | => 20 | """ 21 | Creates a new listening socket manager. 22 | """ 23 | _server = server 24 | _config = config 25 | _sslctx = sslctx 26 | _handlermaker = handler 27 | 28 | fun ref listening(listen: TCPListener ref) => 29 | """ 30 | Inform the server of the bound IP address. 31 | """ 32 | _server._listening(listen.local_address()) 33 | 34 | fun ref not_listening(listen: TCPListener ref) => 35 | """ 36 | Inform the server we failed to listen. 37 | """ 38 | _server._not_listening() 39 | 40 | fun ref closed(listen: TCPListener ref) => 41 | """ 42 | Inform the server we have stopped listening. 43 | """ 44 | _server._closed() 45 | 46 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ ? => 47 | """ 48 | Create a notifier for a specific HTTP socket. A new instance of the 49 | back-end Handler is passed along so it can be used on each `Payload`. 50 | """ 51 | match _sslctx 52 | | None => 53 | _ServerConnHandler(_handlermaker, _server, _config) 54 | | let ctx: SSLContext => 55 | let ssl = ctx.server()? 56 | SSLConnection( 57 | _ServerConnHandler(_handlermaker, _server, _config), 58 | consume ssl) 59 | end 60 | -------------------------------------------------------------------------------- /http_server/_test_connection_handling.pony: -------------------------------------------------------------------------------- 1 | use "pony_test" 2 | use "net" 3 | use "time" 4 | 5 | actor \nodoc\ _ConnectionHandlingTests is TestList 6 | new make() => 7 | None 8 | 9 | fun tag tests(test: PonyTest) => 10 | test(_ConnectionTimeoutTest) 11 | test(_ConnectionCloseHeaderTest) 12 | test(_ConnectionHTTP10Test) 13 | test(_ConnectionHTTP10DefaultCloseTest) 14 | test(_ConnectionCloseHeaderResponseTest) 15 | test(_ConnectionCloseHeaderRawResponseTest) 16 | 17 | class \nodoc\ val _ClosedTestHandlerFactory is HandlerFactory 18 | let _h: TestHelper 19 | 20 | new val create(h: TestHelper) => 21 | _h = h 22 | 23 | fun apply(session: Session): Handler ref^ => 24 | object ref is Handler 25 | fun ref apply(request: Request val, request_id: RequestID) => 26 | _h.complete_action("request-received") 27 | 28 | // send response 29 | session.send_raw( 30 | Responses.builder() 31 | .set_status(StatusOK) 32 | .add_header("Content-Length", "0") 33 | .finish_headers() 34 | .build(), 35 | request_id 36 | ) 37 | session.send_finished(request_id) 38 | 39 | fun ref closed() => 40 | _h.complete_action("connection-closed") 41 | end 42 | 43 | 44 | class \nodoc\ iso _ConnectionTimeoutTest is UnitTest 45 | """ 46 | test that connection is closed when `connection_timeout` is set to `> 0`. 47 | """ 48 | fun name(): String => "connection/timeout" 49 | 50 | fun apply(h: TestHelper) => 51 | h.long_test(Nanos.from_seconds(5)) 52 | h.expect_action("request-received") 53 | h.expect_action("connection-closed") 54 | h.dispose_when_done( 55 | Server( 56 | TCPListenAuth(h.env.root), 57 | object iso is ServerNotify 58 | fun ref listening(server: Server ref) => 59 | try 60 | (let host, let port) = server.local_address().name()? 61 | h.log("listening on " + host + ":" + port) 62 | TCPConnection( 63 | TCPConnectAuth(h.env.root), 64 | object iso is TCPConnectionNotify 65 | fun ref connected(conn: TCPConnection ref) => 66 | conn.write("GET / HTTP/1.1\r\nContent-Length: 0\r\n\r\n") 67 | fun ref connect_failed(conn: TCPConnection ref) => 68 | h.fail("connect failed") 69 | end, 70 | host, 71 | port 72 | ) 73 | end 74 | fun ref closed(server: Server ref) => 75 | h.fail("closed") 76 | end, 77 | _ClosedTestHandlerFactory(h), 78 | ServerConfig( 79 | where connection_timeout' = 1, 80 | timeout_heartbeat_interval' = 500 81 | ) 82 | ) 83 | ) 84 | 85 | class \nodoc\ iso _ConnectionCloseHeaderTest is UnitTest 86 | """ 87 | test that connection is closed when 'Connection: close' header 88 | was sent, even if we didn't specify a timeout. 89 | """ 90 | 91 | fun name(): String => "connection/connection_close_header" 92 | 93 | fun apply(h: TestHelper) => 94 | h.long_test(Nanos.from_seconds(5)) 95 | h.expect_action("request-received") 96 | h.expect_action("connection-closed") 97 | h.dispose_when_done( 98 | Server( 99 | TCPListenAuth(h.env.root), 100 | object iso is ServerNotify 101 | fun ref listening(server: Server ref) => 102 | try 103 | (let host, let port) = server.local_address().name()? 104 | h.log("listening on " + host + ":" + port) 105 | TCPConnection( 106 | TCPConnectAuth(h.env.root), 107 | object iso is TCPConnectionNotify 108 | fun ref connected(conn: TCPConnection ref) => 109 | conn.write("GET / HTTP/1.1\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") 110 | fun ref connect_failed(conn: TCPConnection ref) => 111 | h.fail("connect failed") 112 | end, 113 | host, 114 | port 115 | ) 116 | end 117 | fun ref closed(server: Server ref) => 118 | h.fail("closed") 119 | end, 120 | _ClosedTestHandlerFactory(h), 121 | ServerConfig() 122 | ) 123 | ) 124 | 125 | class \nodoc\ iso _ConnectionCloseHeaderResponseTest is UnitTest 126 | """ 127 | test that connection is closed when the application returned a 'Connection: close' 128 | header. 129 | """ 130 | fun name(): String => "connection/connection_close_response" 131 | 132 | fun apply(h: TestHelper) => 133 | h.long_test(Nanos.from_seconds(5)) 134 | h.expect_action("request-received") 135 | h.expect_action("connection-closed") 136 | h.dispose_when_done( 137 | Server( 138 | TCPListenAuth(h.env.root), 139 | object iso is ServerNotify 140 | fun ref listening(server: Server ref) => 141 | try 142 | (let host, let port) = server.local_address().name()? 143 | h.log("listening on " + host + ":" + port) 144 | TCPConnection( 145 | TCPConnectAuth(h.env.root), 146 | object iso is TCPConnectionNotify 147 | fun ref connected(conn: TCPConnection ref) => 148 | conn.write("GET / HTTP/1.1\r\nContent-Length: 0\r\n\r\n") 149 | fun ref connect_failed(conn: TCPConnection ref) => 150 | h.fail("connect failed") 151 | end, 152 | host, 153 | port 154 | ) 155 | end 156 | fun ref closed(server: Server ref) => 157 | h.fail("closed") 158 | end, 159 | {(session)(h): Handler ref^ => 160 | object ref is Handler 161 | fun ref apply(request: Request val, request_id: RequestID) => 162 | h.complete_action("request-received") 163 | let res = BuildableResponse(where status' = StatusOK) 164 | res.add_header("Connection", "close") 165 | res.set_content_length(0) 166 | session.send_start(consume res, request_id) 167 | session.send_finished(request_id) 168 | 169 | fun ref closed() => 170 | h.complete_action("connection-closed") 171 | end 172 | }, 173 | ServerConfig() 174 | ) 175 | ) 176 | 177 | class \nodoc\ iso _ConnectionCloseHeaderRawResponseTest is UnitTest 178 | fun name(): String => "connection/connection_close_raw_response" 179 | 180 | fun apply(h: TestHelper) => 181 | h.long_test(Nanos.from_seconds(5)) 182 | h.expect_action("request-received") 183 | h.expect_action("connection-closed") 184 | h.dispose_when_done( 185 | Server( 186 | TCPListenAuth(h.env.root), 187 | object iso is ServerNotify 188 | fun ref listening(server: Server ref) => 189 | try 190 | (let host, let port) = server.local_address().name()? 191 | h.log("listening on " + host + ":" + port) 192 | TCPConnection( 193 | TCPConnectAuth(h.env.root), 194 | object iso is TCPConnectionNotify 195 | fun ref connected(conn: TCPConnection ref) => 196 | conn.write("GET / HTTP/1.1\r\nContent-Length: 0\r\n\r\n") 197 | fun ref connect_failed(conn: TCPConnection ref) => 198 | h.fail("connect failed") 199 | end, 200 | host, 201 | port 202 | ) 203 | end 204 | fun ref closed(server: Server ref) => 205 | h.fail("closed") 206 | end, 207 | {(session)(h): Handler ref^ => 208 | object ref is Handler 209 | fun ref apply(request: Request val, request_id: RequestID) => 210 | h.complete_action("request-received") 211 | session.send_raw( 212 | Responses.builder() 213 | .set_status(StatusOK) 214 | .add_header("Connection", "close") 215 | .add_header("Content-Length", "0") 216 | .finish_headers() 217 | .build(), 218 | request_id 219 | where close_session = true) 220 | session.send_finished(request_id) 221 | 222 | fun ref closed() => 223 | h.complete_action("connection-closed") 224 | end 225 | }, 226 | ServerConfig() 227 | ) 228 | ) 229 | 230 | class \nodoc\ iso _ConnectionHTTP10Test is UnitTest 231 | """ 232 | test that connection is closed when HTTP version is 1.0 233 | and no 'Connection: keep-alive' is given. 234 | """ 235 | fun name(): String => "connection/http10/no_keep_alive" 236 | 237 | fun apply(h: TestHelper) => 238 | h.long_test(Nanos.from_seconds(5)) 239 | h.expect_action("request-received") 240 | h.expect_action("connection-closed") 241 | h.dispose_when_done( 242 | Server( 243 | TCPListenAuth(h.env.root), 244 | object iso is ServerNotify 245 | fun ref listening(server: Server ref) => 246 | try 247 | (let host, let port) = server.local_address().name()? 248 | h.log("listening on " + host + ":" + port) 249 | TCPConnection( 250 | TCPConnectAuth(h.env.root), 251 | object iso is TCPConnectionNotify 252 | fun ref connected(conn: TCPConnection ref) => 253 | conn.write("GET / HTTP/1.0\r\nContent-Length: 0\r\nConnection: blaaa\r\n\r\n") 254 | fun ref connect_failed(conn: TCPConnection ref) => 255 | h.fail("connect failed") 256 | end, 257 | host, 258 | port 259 | ) 260 | end 261 | fun ref closed(server: Server ref) => 262 | h.fail("closed") 263 | end, 264 | _ClosedTestHandlerFactory(h), 265 | ServerConfig() 266 | ) 267 | ) 268 | 269 | class \nodoc\ iso _ConnectionHTTP10DefaultCloseTest is UnitTest 270 | """ 271 | Test that connection is closed when HTTP version is 1.0 272 | and not "Connection" header is given. 273 | """ 274 | fun name(): String => "connection/http10/no_connection_header" 275 | 276 | fun apply(h: TestHelper) => 277 | h.long_test(Nanos.from_seconds(5)) 278 | h.expect_action("request-received") 279 | h.expect_action("connection-closed") 280 | h.dispose_when_done( 281 | Server( 282 | TCPListenAuth(h.env.root), 283 | object iso is ServerNotify 284 | fun ref listening(server: Server ref) => 285 | try 286 | (let host, let port) = server.local_address().name()? 287 | h.log("listening on " + host + ":" + port) 288 | TCPConnection( 289 | TCPConnectAuth(h.env.root), 290 | object iso is TCPConnectionNotify 291 | fun ref connected(conn: TCPConnection ref) => 292 | conn.write("GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n") 293 | fun ref connect_failed(conn: TCPConnection ref) => 294 | h.fail("connect failed") 295 | end, 296 | host, 297 | port 298 | ) 299 | end 300 | fun ref closed(server: Server ref) => 301 | h.fail("closed") 302 | end, 303 | _ClosedTestHandlerFactory(h), 304 | ServerConfig() 305 | ) 306 | ) 307 | -------------------------------------------------------------------------------- /http_server/_test_headers.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "debug" 3 | use "pony_check" 4 | use "pony_test" 5 | use "valbytes" 6 | 7 | actor \nodoc\ _HeaderTests is TestList 8 | new make() => 9 | None 10 | 11 | fun tag tests(test: PonyTest) => 12 | test(Property1UnitTest[Array[Header]](_HeadersGetProperty)) 13 | test(Property1UnitTest[Set[String]](_HeadersDeleteProperty)) 14 | 15 | 16 | class \nodoc\ iso _HeadersGetProperty is Property1[Array[Header]] 17 | fun name(): String => "headers/get/property" 18 | 19 | fun gen(): Generator[Array[Header]] => 20 | let name_gen = Generators.ascii_letters(where max=10) 21 | let value_gen = Generators.ascii_letters(where max=10) 22 | Generators.array_of[Header]( 23 | Generators.zip2[String, String]( 24 | name_gen, 25 | value_gen 26 | ) 27 | ) 28 | 29 | fun property(sample: Array[Header], h: PropertyHelper) => 30 | let headers = Headers.create() 31 | let added: Array[Header] = Array[Header](sample.size()) 32 | for header in sample.values() do 33 | headers.add(header._1, header._2) 34 | added.push(header) 35 | for added_header in added.values() do 36 | match headers.get(added_header._1.upper()) 37 | | None => h.fail("not found " + added_header._1) 38 | | let s: String => 39 | var found = false 40 | for splitted in s.split(",").values() do 41 | if added_header._2 == splitted then 42 | found = true 43 | break 44 | end 45 | end 46 | if not found then 47 | h.assert_eq[String](added_header._2, s) 48 | end 49 | end 50 | end 51 | end 52 | 53 | 54 | class \nodoc\ iso _HeadersDeleteProperty is Property1[Set[String]] 55 | fun name(): String => "headers/delete/property" 56 | 57 | fun gen(): Generator[Set[String]] => 58 | // we need unique values in our set, lower and upper case letters are 59 | // considered equal for our Headers impl, so we need to avoid e.g. `a` and 60 | // `A` as the set thinks they are different, but Headers not. 61 | Generators.set_of[String](Generators.ascii(where max=10, range=ASCIILettersLower)) 62 | 63 | fun property(sample: Set[String], h: PropertyHelper) => 64 | let headers = Headers.create() 65 | 66 | let added: Array[String] = Array[String](sample.size()) 67 | let iter = sample.values() 68 | try 69 | let first = iter.next()? 70 | for header in iter do 71 | headers.add(header, header) 72 | added.push(header) 73 | end 74 | 75 | h.log("Added headers:" where verbose = true) 76 | for a in added.values() do 77 | h.log(a where verbose = true) 78 | end 79 | 80 | // the header we never added is not inside 81 | h.assert_true(headers.delete(first) is None, "Header: " + first + " got deleted from headers although never added") 82 | for added_header in added.values() do 83 | 84 | // available before delete 85 | h.assert_true(headers.get(added_header) isnt None, "Header: " + added_header + " was added to headers, but wasn't retrieved with get") 86 | h.assert_true(headers.delete(added_header) isnt None, "Header: " + added_header + " was added to headers, but wasn't found during delete") 87 | // gone after delete 88 | h.assert_true(headers.get(added_header) is None, "Header: " + added_header + " was deleted but could be retrieved with get") 89 | 90 | // the header we never added is not inside 91 | h.assert_true(headers.delete(first) is None, "Header: " + first + " got deleted from headers although never added") 92 | end 93 | end 94 | 95 | -------------------------------------------------------------------------------- /http_server/_test_pipelining.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "time" 3 | use "pony_test" 4 | use "random" 5 | use "valbytes" 6 | 7 | actor \nodoc\ _PipeliningTests is TestList 8 | new make() => 9 | None 10 | 11 | fun tag tests(test: PonyTest) => 12 | test(_PipeliningOrderTest) 13 | test(_PipeliningCloseTest) 14 | 15 | 16 | class \nodoc\ val _PipeliningOrderHandlerFactory is HandlerFactory 17 | let _h: TestHelper 18 | let _timers: Timers 19 | 20 | new val create(h: TestHelper) => 21 | _h = h 22 | _timers = Timers 23 | 24 | fun apply(session: Session): Handler ref^ => 25 | let random = Rand(Time.seconds().u64()) 26 | object ref is Handler 27 | let _session: Session = session 28 | 29 | fun ref finished(request_id: RequestID) => 30 | let rid = request_id.string() 31 | let res = Responses.builder() 32 | .set_status(StatusOK) 33 | .add_header("Content-Length", rid.size().string()) 34 | .finish_headers() 35 | .add_chunk(rid.string().array()) 36 | .build() 37 | // wait a random time (max 100 milliseconds) 38 | // then send a response with the given request_id as body 39 | _timers( 40 | Timer( 41 | object iso is TimerNotify 42 | fun ref apply(timer: Timer, count: U64): Bool => 43 | _session.send_raw(res, request_id) 44 | _session.send_finished(request_id) 45 | false 46 | end, 47 | Nanos.from_millis(random.int[U64](U64(100))), 48 | 0 49 | ) 50 | ) 51 | end 52 | 53 | class \nodoc\ iso _PipeliningOrderTest is UnitTest 54 | let requests: Array[String] val = [ 55 | "GET / HTTP/1.1\r\nContent-Length: 1\r\n\r\n1" 56 | "POST /path?query=param%20eter HTTP/1.1\r\nContent-Length: 1\r\n\r\n2" 57 | "GET /bla HTTP/1.1\r\nContent-Length: 1\r\nAccept: */*\r\n\r\n3" 58 | "GET / HTTP/1.1\r\nContent-Length: 1\r\n\r\n4" 59 | "GET / HTTP/1.1\r\nContent-Length: 1\r\n\r\n5" 60 | ] 61 | fun name(): String => "pipelining/order" 62 | 63 | fun apply(h: TestHelper) => 64 | h.long_test(Nanos.from_seconds(5)) 65 | h.expect_action("all received") 66 | h.dispose_when_done( 67 | Server( 68 | TCPListenAuth(h.env.root), 69 | object iso is ServerNotify 70 | let reqs: Array[String] val = requests 71 | fun ref listening(server: Server ref) => 72 | try 73 | (let host, let port) = server.local_address().name()? 74 | h.log("listening on " + host + ":" + port) 75 | TCPConnection( 76 | TCPConnectAuth(h.env.root), 77 | object iso is TCPConnectionNotify 78 | var buffer: ByteArrays = ByteArrays 79 | var order: Array[USize] iso = recover iso Array[USize](5) end 80 | 81 | fun ref connected(conn: TCPConnection ref) => 82 | h.log("connected") 83 | // pipeline all requests out 84 | conn.write("".join(reqs.values())) 85 | fun ref received( 86 | conn: TCPConnection ref, 87 | data: Array[U8 val] iso, 88 | times: USize) 89 | : Bool => 90 | buffer = buffer + (consume data) 91 | while buffer.size() > 5 do 92 | match buffer.find("\r\n\r\n") 93 | | (true, let idx: USize) => 94 | if buffer.size() >= idx then 95 | buffer = buffer.drop(idx + 4) 96 | try 97 | let id = buffer.take(1).string().usize()? 98 | buffer = buffer.drop(1) 99 | h.log("received response: " + id.string()) 100 | order.push(id) 101 | else 102 | h.fail("incomplete request") 103 | end 104 | else 105 | break 106 | end 107 | else 108 | break 109 | end 110 | end 111 | if order.size() == 5 then 112 | h.complete_action("all received") 113 | // assert that we receive in sending order, 114 | // no matter which response was processed first 115 | // by the server 116 | h.assert_array_eq[USize]( 117 | [as USize: 0; 1; 2; 3; 4], 118 | order = recover iso Array[USize](0) end 119 | ) 120 | end 121 | true 122 | 123 | fun ref connect_failed(conn: TCPConnection ref) => 124 | h.fail("connect failed") 125 | end, 126 | host, 127 | port 128 | ) 129 | end 130 | fun ref closed(server: Server ref) => 131 | h.fail("closed") 132 | end, 133 | _PipeliningOrderHandlerFactory(h), 134 | ServerConfig() 135 | ) 136 | ) 137 | 138 | 139 | class \nodoc\ iso _PipeliningCloseTest is UnitTest 140 | """ 141 | Test that connection is closed after handling a request 142 | with "Connection: close" header, not earlier, not later. 143 | """ 144 | fun name(): String => "pipelining/close" 145 | 146 | fun apply(h: TestHelper) => 147 | h.long_test(Nanos.from_seconds(5)) 148 | h.expect_action("connected") 149 | h.expect_action("all received") 150 | h.dispose_when_done( 151 | Server( 152 | TCPListenAuth(h.env.root), 153 | object iso is ServerNotify 154 | fun ref listening(server: Server ref) => 155 | try 156 | (let host, let port) = server.local_address().name()? 157 | h.log("listening on " + host + ":" + port) 158 | TCPConnection( 159 | TCPConnectAuth(h.env.root), 160 | object iso is TCPConnectionNotify 161 | var buffer: ByteArrays = ByteArrays 162 | var order: Array[USize] iso = recover iso Array[USize](5) end 163 | let reqs: Array[String] val = [ 164 | "GET / HTTP/1.1\r\n\r\n" 165 | "GET /path?query=param HTTP/1.1\r\nHeader: value\r\nContent-Length: 1\r\n\r\n " 166 | "PATCH / HTTP/1.1\r\nConnection: Close\r\n\r\n" 167 | "GET /ignore-me HTTP/1.1\r\nContent-Length: 0\r\n\r\n" 168 | ] 169 | 170 | fun ref connected(conn: TCPConnection ref) => 171 | h.complete_action("connected") 172 | // pipeline all requests out 173 | conn.write("".join(reqs.values())) 174 | 175 | fun ref connect_failed(conn: TCPConnection ref) => 176 | h.fail("couldn't connect to server") 177 | 178 | fun ref received(conn: TCPConnection ref, data: Array[U8 val] iso, times: USize): Bool => 179 | buffer = buffer + (consume data) 180 | while buffer.size() > 5 do 181 | match buffer.find("\r\n\r\n") 182 | | (true, let idx: USize) => 183 | if buffer.size() >= idx then 184 | buffer = buffer.drop(idx + 4) 185 | try 186 | let id = buffer.take(1).string().usize()? 187 | buffer = buffer.drop(1) 188 | h.log("received response: " + id.string()) 189 | order.push(id) 190 | else 191 | h.fail("incomplete request") 192 | end 193 | else 194 | break 195 | end 196 | else 197 | break 198 | end 199 | end 200 | 201 | // assert we receive at least the three first elements 202 | if order.size() >= 3 then 203 | h.complete_action("all received") 204 | let o = (order = recover iso Array[USize](0) end) 205 | let res = recover val consume o end 206 | try 207 | h.assert_eq[USize](0, res(0)?) 208 | h.assert_eq[USize](1, res(1)?) 209 | h.assert_eq[USize](2, res(2)?) 210 | end 211 | end 212 | true 213 | end, 214 | host, 215 | port 216 | ) 217 | end 218 | fun ref closed(server: Server ref) => 219 | h.fail("closed") 220 | end, 221 | _PipeliningOrderHandlerFactory(h), 222 | ServerConfig() 223 | ) 224 | ) 225 | -------------------------------------------------------------------------------- /http_server/_test_response.pony: -------------------------------------------------------------------------------- 1 | use "pony_test" 2 | 3 | actor \nodoc\ _ResponseTests is TestList 4 | new make() => 5 | None 6 | 7 | fun tag tests(test: PonyTest) => 8 | test(_BuildableResponseTest) 9 | test(_ResponseBuilderTest) 10 | 11 | class \nodoc\ iso _BuildableResponseTest is UnitTest 12 | fun name(): String => "responses/BuildableResponse" 13 | 14 | fun apply(h: TestHelper) => 15 | let without_length = BuildableResponse() 16 | h.assert_true(without_length.header("Content-Length") is None, "Content-Length header set although not provided in constructor") 17 | 18 | let array = recover val String.from_iso_array(without_length.array()) end 19 | h.log(array) 20 | h.assert_true(array.contains("\r\nContent-Length: 0\r\n"), "No content-length header added in array response") 21 | 22 | let bytes = without_length.to_bytes().string() 23 | h.log(bytes) 24 | h.assert_true(bytes.contains("\r\nContent-Length: 0\r\n"), "No content-length header added in to_bytes response") 25 | 26 | 27 | let with_length = BuildableResponse().set_content_length(42) 28 | match with_length.header("Content-Length") 29 | | let hvalue: String => 30 | h.assert_eq[String]("42", hvalue) 31 | | None => 32 | h.fail("No Content-Length header set") 33 | end 34 | 35 | let chunked_without_length = BuildableResponse().set_transfer_encoding(Chunked) 36 | h.assert_true(without_length.header("Content-Length") is None, "Content-Length header set although not provided in constructor") 37 | 38 | let array2 = recover val String.from_iso_array(chunked_without_length.array()) end 39 | h.log(array2) 40 | h.assert_false(array2.contains("\r\nContent-Length: "), "Content-length header added in array response although chunked") 41 | 42 | let bytes2 = chunked_without_length.to_bytes().string() 43 | h.log(bytes2) 44 | h.assert_false(bytes2.contains("\r\nContent-Length: "), "Content-length header added in to_bytes response although chunked") 45 | 46 | // first set content-length, then transfer coding 47 | let complex = 48 | BuildableResponse().set_content_length(42).set_transfer_encoding(Chunked) 49 | h.assert_true(complex.header("Content-Length") is None, "Content-Length header set although not provided in constructor") 50 | 51 | let array3 = recover val String.from_iso_array(complex.array()) end 52 | h.log(array3) 53 | h.assert_false(array3.contains("\r\nContent-Length: "), "Content-length header added in array response although chunked") 54 | 55 | let bytes3 = complex.to_bytes().string() 56 | h.log(bytes3) 57 | h.assert_false(bytes3.contains("\r\nContent-Length: "), "Content-length header added in to_bytes response although chunked") 58 | 59 | 60 | class \nodoc\ iso _ResponseBuilderTest is UnitTest 61 | fun name(): String => "responses/ResponseBuilder" 62 | 63 | fun apply(h: TestHelper) => 64 | let without_length = Responses.builder().set_status(StatusOK).add_header("Server", "FooBar").finish_headers().build() 65 | var s = String.create() 66 | for bs in without_length.values() do 67 | s.append(bs) 68 | end 69 | h.assert_true(s.contains("\r\nContent-Length: 0\r\n"), "No content length added to Request: " + s) 70 | 71 | let with_length = Responses.builder().set_status(StatusOK).set_content_length(4).finish_headers().add_chunk("COOL").build() 72 | s = String.create() 73 | for bs in with_length.values() do 74 | s.append(bs) 75 | end 76 | h.assert_true(s.contains("\r\nContent-Length: 4\r\n"), "No or wrong content length added to Request: " + s) 77 | 78 | let chunked = 79 | Responses.builder().set_status(StatusOK).set_transfer_encoding(Chunked).add_header("Foo", "Bar").finish_headers().add_chunk("FOO").add_chunk("BAR").add_chunk("").build() 80 | let c = recover val 81 | let tmp = String.create() 82 | for bs in chunked.values() do 83 | tmp.append(bs) 84 | end 85 | tmp 86 | end 87 | h.assert_eq[String]("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nFoo: Bar\r\n\r\n3\r\nFOO\r\n3\r\nBAR\r\n0\r\n\r\n", c) 88 | -------------------------------------------------------------------------------- /http_server/_test_server_error_handling.pony: -------------------------------------------------------------------------------- 1 | use "files" 2 | use "net" 3 | use "net_ssl" 4 | use "pony_test" 5 | 6 | actor \nodoc\ _ServerErrorHandlingTests is TestList 7 | new make() => 8 | None 9 | 10 | fun tag tests(test: PonyTest) => 11 | test(_ServerConnectionClosedTest) 12 | 13 | class \nodoc\ val _ServerConnectionClosedHandlerFactory is HandlerFactory 14 | let _h: TestHelper 15 | new val create(h: TestHelper) => 16 | _h = h 17 | 18 | fun apply(session: Session): Handler ref^ => 19 | object is Handler 20 | fun ref apply(res: Request val, request_id: RequestID) => 21 | _h.log("received request") 22 | fun ref closed() => 23 | _h.complete_action("server failed with ConnectionClosed") 24 | end 25 | 26 | class \nodoc\ iso _ServerConnectionClosedTest is UnitTest 27 | fun name(): String => "server/error-handling/connection-closed" 28 | fun apply(h: TestHelper) => 29 | h.long_test(5_000_000_000) 30 | h.expect_action("server listening") 31 | h.expect_action("client connected") 32 | h.expect_action("server failed with ConnectionClosed") 33 | 34 | let server = Server( 35 | TCPListenAuth(h.env.root), 36 | object iso is ServerNotify 37 | let _h: TestHelper = h 38 | fun ref listening(server: Server ref) => 39 | _h.complete_action("server listening") 40 | 41 | try 42 | (let host, let port) = server.local_address().name()? 43 | _h.log("listening on " + host + ":" + port) 44 | let conn = 45 | TCPConnection( 46 | TCPConnectAuth(_h.env.root), 47 | object iso is TCPConnectionNotify 48 | fun ref connected(conn: TCPConnection ref) => 49 | _h.complete_action("client connected") 50 | conn.write("GET /abc/def HTTP/1.1\r\n\r\n") 51 | conn.dispose() 52 | 53 | fun ref received(conn: TCPConnection ref, data: Array[U8] iso, times: USize): Bool => 54 | true 55 | 56 | fun ref connect_failed(conn: TCPConnection ref) => 57 | _h.fail("client connect failed") 58 | 59 | fun ref closed(conn: TCPConnection ref) => 60 | _h.complete_action("client connection closed") 61 | end, 62 | host, 63 | port) 64 | _h.dispose_when_done(conn) 65 | 66 | else 67 | _h.fail("error starting client") 68 | end 69 | 70 | fun ref not_listening(server: Server ref) => 71 | _h.fail_action("server listening") 72 | 73 | fun ref closed(server: Server ref) => 74 | _h.log("server stopped listening") 75 | end, 76 | _ServerConnectionClosedHandlerFactory(h) 77 | where config = ServerConfig(where host'="127.0.0.1") 78 | ) 79 | h.dispose_when_done(server) 80 | -------------------------------------------------------------------------------- /http_server/handler.pony: -------------------------------------------------------------------------------- 1 | interface Handler 2 | """ 3 | This is the interface through which HTTP requests are delivered *to* 4 | application code and through which HTTP responses are sent to the underlying connection. 5 | 6 | Instances of a Handler are executed in the context of the `Session` actor so most of them should be 7 | passing data on to a processing actor. 8 | 9 | Each `Session` must have a unique instance of the handler. The 10 | application code does not necessarily know when an `Session` is created, 11 | so the application must provide an instance of `HandlerFactory` that 12 | will be called at the appropriate time. 13 | 14 | ### Receiving Requests 15 | 16 | When an [Request](http_server-Request.md) is received on an [Session](http_server-Session.md) actor, 17 | the corresponding [Handler.apply](http_server-Handler.md#apply) method is called 18 | with the request and a [RequestID](http_server-RequestID.md). The [Request](http_server-Request.md) 19 | contains the information extracted from HTTP Headers and the Request Line, but it does not 20 | contain any body data. It is sent to [Handler.apply](http_server-Handler.md#apply) before the body 21 | is fully received. 22 | 23 | If the request has a body, its raw data is sent to the [Handler.chunk](http_server-Handler.md#chunk) method 24 | together with the [RequestID](http_server-RequestID.md) of the request it belongs to. 25 | 26 | Once all body data is received, [Handler.finished](http_server-Handler.md#finished) is called with the 27 | [RequestID](http_server-RequestID.md) of the request it belongs to. Now is the time to act on the full body data, 28 | if it hasn't been processed yet. 29 | 30 | The [RequestID](http_server-RequestID.md) must be kept around for sending the response for this request. 31 | This way the session can ensure, all responses are sent in the same order as they have been received, 32 | which is required for HTTP pipelining. This way processing responses can be passed to other actors and 33 | processing can take arbitrary times. The [Session](http_server-Session.md) will take care of sending 34 | the responses in the correct order. 35 | 36 | It is guaranteed that the call sequence is always: 37 | 38 | - exactly once: `apply(request_n, requestid_n)` 39 | - zero or more times: `chunk(data, requestid_n)` 40 | - exactly once: `finished(requestid_n)` 41 | 42 | And so on for `requestid_(n + 1)`. Only after `finished` has been called for a 43 | `RequestID`, the next request will be received by the Handler instance, there will 44 | be no interleaving. So it is save to keep state for the given request in a Handler between calls to `apply` 45 | and `finished`. 46 | 47 | #### Failures and Cancelling 48 | 49 | If a [Session](http_server-Session.md) experienced faulty requests, the [Handler](http_server-Handler.md) 50 | is notified via [Handler.failed](http_server-Handler.md#failed). 51 | 52 | If the underlying connection to a [Session](http_server-Session.md) has been closed, 53 | the [Handler](http_server-Handler.md) is notified via [Handler.closed](http_server-Handler.md#closed). 54 | 55 | ### Sending Responses 56 | 57 | A handler is instantiated using a [HandlerFactory](http_server-HandlerFactory.md), which passes an instance of 58 | [Session](http_server-Session.md) to be used in constructing a handler. 59 | 60 | A Session is required to be able to send responses. 61 | See the docs for [Session](http_server-Session.md) for ways to send responses. 62 | 63 | Example Handler: 64 | 65 | ```pony 66 | use "http" 67 | use "valbytes" 68 | 69 | class MyHandler is Handler 70 | let _session: Session 71 | 72 | var _path: String = "" 73 | var _body: ByteArrays = ByteArrays 74 | 75 | new create(session: Session) => 76 | _session = session 77 | 78 | fun ref apply(request: Request val, request_id: RequestID): Any => 79 | _path = request.uri().path 80 | 81 | fun ref chunk(data: ByteSeq val, request_id: RequestID) => 82 | _body = _body + data 83 | 84 | fun ref finished(request_id: RequestID) => 85 | _session.send_raw( 86 | Responses.builder() 87 | .set_status(StatusOk) 88 | .add_header("Content-Length", (_body.size() + _path.size() + 13).string()) 89 | .add_header("Content-Type", "text/plain") 90 | .finish_headers() 91 | .add_chunk("received ") 92 | .add_chunk((_body = ByteArrays).array()) 93 | .add_chunk(" at ") 94 | .add_chunk(_path) 95 | .build(), 96 | request_id 97 | ) 98 | _session.send_finished(request_id) 99 | ``` 100 | 101 | """ 102 | fun ref apply(request: Request val, request_id: RequestID): Any => 103 | """ 104 | Notification of an incoming message. 105 | 106 | Only one HTTP message will be processed at a time, and that starts 107 | with a call to this method. 108 | """ 109 | 110 | fun ref chunk(data: ByteSeq val, request_id: RequestID) => 111 | """ 112 | Notification of incoming body data. The body belongs to the most 113 | recent `Request` delivered by an `apply` notification. 114 | """ 115 | 116 | fun ref finished(request_id: RequestID) => 117 | """ 118 | Notification that no more body chunks are coming. Delivery of this HTTP 119 | message is complete. 120 | """ 121 | 122 | fun ref cancelled(request_id: RequestID) => 123 | """ 124 | Notification that sending a response has been cancelled locally, 125 | e.g. by closing the server or manually cancelling a single request. 126 | """ 127 | 128 | fun ref failed(reason: RequestParseError, request_id: RequestID) => 129 | """ 130 | Notification about failure parsing HTTP requests. 131 | """ 132 | 133 | fun ref closed() => 134 | """ 135 | Notification that the underlying connection has been closed. 136 | """ 137 | 138 | fun ref throttled() => 139 | """ 140 | Notification that the session temporarily can not accept more data. 141 | """ 142 | 143 | fun ref unthrottled() => 144 | """ 145 | Notification that the session can resume accepting data. 146 | """ 147 | 148 | 149 | interface HandlerFactory 150 | """ 151 | The TCP connections that underlie HTTP sessions get created within 152 | the `http` package at times that the application code can not 153 | predict. Yet, the application code has to provide custom hooks into 154 | these connections as they are created. To accomplish this, the 155 | application code provides an instance of a `class` that implements 156 | this interface. 157 | 158 | The `HandlerFactory.apply` method will be called when a new 159 | `Session` is created, giving the application a chance to create 160 | an instance of its own `Handler`. This happens on both 161 | client and server ends. 162 | """ 163 | 164 | fun apply(session: Session): Handler ref^ 165 | """ 166 | Called by the [Session](http_server-Session.md) when it needs a new instance of the 167 | application's [Handler](http_server-Handler.md). It is suggested that the 168 | `session` value be passed to the constructor for the new 169 | [Handler](http_server-Handler.md), you will need it for sending stuff back. 170 | 171 | This part must be implemented, as there might be more paramaters 172 | that need to be passed for creating a Handler. 173 | """ 174 | 175 | interface HandlerWithoutContext is Handler 176 | """ 177 | Simple [Handler](http_server-Handler.md) that can be constructed 178 | with only a Session. 179 | """ 180 | new create(session: Session) 181 | 182 | 183 | primitive SimpleHandlerFactory[T: HandlerWithoutContext] 184 | """ 185 | HandlerFactory for a HandlerWithoutContext. 186 | 187 | Just create it like: 188 | 189 | ```pony 190 | let server = 191 | Server( 192 | ..., 193 | SimpleHandlerFactory[MySimpleHandler], 194 | ... 195 | ) 196 | ``` 197 | 198 | """ 199 | fun apply(session: Session): Handler ref^ => 200 | T.create(session) 201 | -------------------------------------------------------------------------------- /http_server/headers.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | 3 | type Header is (String, String) 4 | """ 5 | Defining a HTTP header as Tuple Strings for name and value. 6 | """ 7 | 8 | class Headers 9 | """ 10 | Collection for headers 11 | based on a sorted array we use bisect to insert and get values. 12 | We compare the strings case-insensitive when sorting, inserting and getting headers. 13 | 14 | We want to use the bytes we get to build the headers as is without changing them, in order to avoid allocation. 15 | 16 | This isn't using a hashmap because getting the hash in a case-insensitive manner 17 | would require to iterate over single bytes, which isn't as fast as it could be. 18 | Also the amount of headers in a request is usually small, so the penalty of doing 19 | a binary search isn't as bad. 20 | 21 | Getting a header is case insensitive, so you don't need to care about header name casing 22 | when asking for a header. 23 | 24 | ### Usage 25 | 26 | ```pony 27 | let headers = Headers 28 | header.set("Connection", "Close") // setting a header, possibly overwriting previous values 29 | header.add("Multiple", "1") // adding a header, concatenating previous and this value with a comma. 30 | header.add("Multiple", "2") 31 | 32 | // getting a header is case-insensitive 33 | match header.get("cOnNeCTiOn") 34 | | let value: String => // do something with value 35 | else 36 | // not found 37 | end 38 | 39 | // iterating over headers 40 | for (name, value) in headers.values() do 41 | env.out.print(name + ": " + value) 42 | end 43 | 44 | // remove all headers from this structure 45 | headers.clear() 46 | ``` 47 | 48 | """ 49 | // avoid reallocating new strings just because header names are case 50 | // insensitive. 51 | // handle insensitivity during add and get 52 | // - TODO: find a way to do this with hashmap 53 | var _hl: Array[Header] 54 | 55 | new ref create() => 56 | _hl = _hl.create(4) 57 | 58 | new ref from_map(headers: Map[String, String]) => 59 | _hl = _hl.create(headers.size()) 60 | for header in headers.pairs() do 61 | add(header._1, header._2) 62 | end 63 | 64 | new ref from_seq(headers: ReadSeq[Header]) => 65 | _hl = _hl.create(headers.size()) 66 | for header in headers.values() do 67 | add(header._1, header._2) 68 | end 69 | 70 | new ref from_iter(headers: Iterator[Header], size: USize = 4) => 71 | _hl = _hl.create(size) 72 | for header in headers do 73 | add(header._1, header._2) 74 | end 75 | 76 | fun ref set(name: String, value: String) => 77 | """ 78 | if a header with name already exists, its value will be overriden with this value. 79 | """ 80 | try 81 | var i = USize(0) 82 | var l = USize(0) 83 | var r = _hl.size() 84 | while l < r do 85 | i = (l + r).fld(2) 86 | let header = _hl(i)? 87 | match IgnoreAsciiCase.compare(header._1, name) 88 | | Less => 89 | l = i + 1 90 | | Equal => 91 | _hl(i)? = (name, value) 92 | return 93 | else 94 | r = i 95 | end 96 | end 97 | _hl.insert(l, (name, value))? 98 | end 99 | 100 | fun ref delete(name: String): (Header | None) => 101 | """ 102 | If a header with name exists, remove it. 103 | 104 | Returns `true` if a header with that name existed, `false` otherwise. 105 | """ 106 | // binary search 107 | try 108 | var i = USize(0) 109 | var l = USize(0) 110 | var r = _hl.size() 111 | while l < r do 112 | i = (l + r).fld(2) 113 | let header = _hl(i)? 114 | match IgnoreAsciiCase.compare(header._1, name) 115 | | Less => 116 | l = i + 1 117 | | Equal => 118 | return try _hl.delete(i)? end 119 | else 120 | r = i 121 | end 122 | end 123 | end 124 | None 125 | 126 | fun ref add(name: String, value: String) => 127 | """ 128 | If a header with this name already exists, value will be 129 | appended after a separating comma. 130 | """ 131 | // binary search 132 | try 133 | var i = USize(0) 134 | var l = USize(0) 135 | var r = _hl.size() 136 | while l < r do 137 | i = (l + r).fld(2) 138 | let header = _hl(i)? 139 | match IgnoreAsciiCase.compare(header._1, name) 140 | | Less => 141 | l = i + 1 142 | | Equal => 143 | let old_value = header._2 144 | let new_value = recover iso String(old_value.size() + 1 + value.size()) end 145 | new_value.>append(old_value) 146 | .>append(",") 147 | .>append(value) 148 | _hl(i)? = (header._1, consume new_value) 149 | return 150 | else 151 | r = i 152 | end 153 | end 154 | _hl.insert(l, (name, value))? 155 | end 156 | 157 | fun get(name: String): (String | None) => 158 | // binary search 159 | var l = USize(0) 160 | var r = _hl.size() 161 | var i = USize(0) 162 | try 163 | while l < r do 164 | i = (l + r).fld(2) 165 | let header = _hl(i)? 166 | match IgnoreAsciiCase.compare(header._1, name) 167 | | Less => 168 | l = i + 1 169 | | Equal => 170 | return header._2 171 | | Greater => 172 | r = i 173 | end 174 | end 175 | end 176 | None 177 | 178 | fun ref clear() => 179 | _hl.clear() 180 | 181 | fun values(): Iterator[Header] => _hl.values() 182 | 183 | fun byte_size(): USize => 184 | """ 185 | size of the given headers including header-separator and crlf. 186 | """ 187 | var s = USize(0) 188 | for (k, v) in _hl.values() do 189 | s + k.size() + 2 + v.size() + 2 190 | end 191 | s 192 | 193 | 194 | -------------------------------------------------------------------------------- /http_server/method.pony: -------------------------------------------------------------------------------- 1 | interface val Method is (Equatable[Method] & Stringable) 2 | """ 3 | HTTP method 4 | 5 | See: https://tools.ietf.org/html/rfc2616#section-5.1.1 6 | """ 7 | fun repr(): String val 8 | fun string(): String iso^ 9 | fun eq(o: Method): Bool 10 | 11 | primitive CONNECT is Method 12 | fun repr(): String val => "CONNECT" 13 | fun string(): String iso^ => repr().clone() 14 | fun eq(o: Method): Bool => o is this 15 | 16 | primitive GET is Method 17 | fun repr(): String val => "GET" 18 | fun string(): String iso^ => repr().clone() 19 | fun eq(o: Method): Bool => o is this 20 | 21 | primitive DELETE is Method 22 | fun repr(): String => "DELETE" 23 | fun string(): String iso^ => repr().clone() 24 | fun eq(o: Method): Bool => o is this 25 | 26 | primitive HEAD is Method 27 | fun repr(): String => "HEAD" 28 | fun string(): String iso^ => repr().clone() 29 | fun eq(o: Method): Bool => o is this 30 | 31 | primitive OPTIONS is Method 32 | fun repr(): String => "OPTIONS" 33 | fun string(): String iso^ => repr().clone() 34 | fun eq(o: Method): Bool => o is this 35 | 36 | primitive PATCH is Method 37 | fun repr(): String => "PATCH" 38 | fun string(): String iso^ => repr().clone() 39 | fun eq(o: Method): Bool => o is this 40 | 41 | primitive POST is Method 42 | fun repr(): String => "POST" 43 | fun string(): String iso^ => repr().clone() 44 | fun eq(o: Method): Bool => o is this 45 | 46 | primitive PUT is Method 47 | fun repr(): String => "PUT" 48 | fun string(): String iso^ => repr().clone() 49 | fun eq(o: Method): Bool => o is this 50 | 51 | primitive TRACE is Method 52 | fun repr(): String => "TRACE" 53 | fun string(): String iso^ => repr().clone() 54 | fun eq(o: Method): Bool => o is this 55 | 56 | primitive Methods 57 | fun parse(maybe_method: ReadSeq[U8]): (Method | None) => 58 | if _Equality.readseqs(maybe_method, GET.repr()) then GET 59 | elseif _Equality.readseqs(maybe_method, PUT.repr()) then PUT 60 | elseif _Equality.readseqs(maybe_method, PATCH.repr()) then PUT 61 | elseif _Equality.readseqs(maybe_method, POST.repr()) then POST 62 | elseif _Equality.readseqs(maybe_method, HEAD.repr()) then HEAD 63 | elseif _Equality.readseqs(maybe_method, DELETE.repr()) then DELETE 64 | elseif _Equality.readseqs(maybe_method, CONNECT.repr()) then CONNECT 65 | elseif _Equality.readseqs(maybe_method, OPTIONS.repr()) then OPTIONS 66 | elseif _Equality.readseqs(maybe_method, TRACE.repr()) then TRACE 67 | end 68 | 69 | 70 | primitive _Equality 71 | fun readseqs(left: ReadSeq[U8], right: ReadSeq[U8]): Bool => 72 | let size = left.size() 73 | if size != right.size() then 74 | false 75 | else 76 | var ri: USize = 0 77 | try 78 | // TODO: vectorize if possible 79 | while ri < size do 80 | if left(ri)? != right(ri)? then 81 | return false 82 | end 83 | ri = ri + 1 84 | end 85 | true 86 | else 87 | false 88 | end 89 | end 90 | 91 | -------------------------------------------------------------------------------- /http_server/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_server/request.pony: -------------------------------------------------------------------------------- 1 | 2 | interface val _Version is (Equatable[Version] & Stringable & Comparable[Version]) 3 | fun to_bytes(): Array[U8] val 4 | 5 | primitive HTTP11 is _Version 6 | """ 7 | HTTP/1.1 8 | """ 9 | fun string(): String iso^ => recover iso String(8).>append("HTTP/1.1") end 10 | fun to_bytes(): Array[U8] val => [as U8: 'H'; 'T'; 'T'; 'P'; '/'; '1'; '.'; '1'] 11 | fun u64(): U64 => 12 | """ 13 | Representation of the bytes for this HTTP Version on the wire in form of an 8-byte unsigned integer with the ASCII bytes written from highest to least significant byte. 14 | 15 | Result: `0x485454502F312E31` 16 | """ 17 | 'HTTP/1.1' 18 | fun eq(o: Version): Bool => o is this 19 | fun lt(o: Version): Bool => 20 | match o 21 | | let _: HTTP11 => false 22 | | let _: HTTP10 => false 23 | | let _: HTTP09 => false 24 | end 25 | 26 | 27 | primitive HTTP10 is _Version 28 | """ 29 | HTTP/1.0 30 | """ 31 | fun string(): String iso^ => recover iso String(8).>append("HTTP/1.0") end 32 | fun to_bytes(): Array[U8] val => [as U8: 'H'; 'T'; 'T'; 'P'; '/'; '1'; '.'; '0'] 33 | fun u64(): U64 => 34 | """ 35 | Representation of the bytes for this HTTP Version on the wire in form of an 8-byte unsigned integer with the ASCII bytes written from highest to least significant byte. 36 | 37 | Result: `0x485454502F312E30` 38 | """ 39 | 'HTTP/1.0' 40 | fun eq(o: Version): Bool => o is this 41 | fun lt(o: Version): Bool => 42 | match o 43 | | let _: HTTP11 => true 44 | | let _: HTTP10 => false 45 | | let _: HTTP09 => false 46 | end 47 | 48 | primitive HTTP09 is _Version 49 | """ 50 | HTTP/0.9 51 | """ 52 | fun string(): String iso^ => recover iso String(8).>append("HTTP/0.9") end 53 | fun to_bytes(): Array[U8] val => [as U8: 'H'; 'T'; 'T'; 'P'; '/'; '0'; '.'; '9'] 54 | fun u64(): U64 => 55 | """ 56 | Representation of the bytes for this HTTP Version on the wire in form of an 8-byte unsigned integer with the ASCII bytes written from highest to least significant byte. 57 | 58 | Result: `0x485454502F302E39` 59 | """ 60 | 'HTTP/0.9' 61 | fun eq(o: Version): Bool => o is this 62 | fun lt(o: Version): Bool => 63 | match o 64 | | let _: HTTP11 => true 65 | | let _: HTTP10 => true 66 | | let _: HTTP09 => false 67 | end 68 | 69 | 70 | 71 | type Version is ((HTTP09 | HTTP10 | HTTP11) & _Version) 72 | """ 73 | union of supported HTTP Versions 74 | 75 | See: https://tools.ietf.org/html/rfc2616#section-3.1 76 | """ 77 | 78 | 79 | interface val Request 80 | """ 81 | HTTP Request 82 | 83 | * Method 84 | * URI 85 | * HTTP-Version 86 | * Headers 87 | * Transfer-Coding 88 | * Content-Length 89 | 90 | Without body. 91 | """ 92 | fun method(): Method 93 | fun uri(): URL 94 | fun version(): Version 95 | fun header(name: String): (String | None) 96 | fun headers(): Iterator[Header] 97 | fun transfer_coding(): (Chunked | None) 98 | fun content_length(): (USize | None) 99 | fun has_body(): Bool 100 | 101 | class val BuildableRequest is Request 102 | """ 103 | A HTTP Request that is created with `trn` refcap 104 | in order to be mutable, and then, when done, be consumed into 105 | a `val` reference. This is the way, the `HTTP11RequestParser` is handling this class and so should you. 106 | """ 107 | var _method: Method 108 | var _uri: URL 109 | var _version: Version 110 | embed _headers: Headers = _headers.create() 111 | var _transfer_coding: (Chunked | None) 112 | var _content_length: (USize | None) 113 | 114 | new trn create( 115 | method': Method = GET, 116 | uri': URL = URL, 117 | version': Version = HTTP11, 118 | transfer_coding': (Chunked | None) = None, 119 | content_length': (USize | None) = None) => 120 | _method = method' 121 | _uri = uri' 122 | _version = version' 123 | _transfer_coding = transfer_coding' 124 | _content_length = content_length' 125 | 126 | fun method(): Method => 127 | """ 128 | The Request Method. 129 | 130 | See: https://tools.ietf.org/html/rfc2616#section-5.1.1 131 | """ 132 | _method 133 | 134 | fun ref set_method(method': Method): BuildableRequest ref => 135 | _method = method' 136 | this 137 | 138 | fun uri(): URL => 139 | """ 140 | The request URI 141 | 142 | See: https://tools.ietf.org/html/rfc2616#section-5.1.2 143 | """ 144 | _uri 145 | 146 | fun ref set_uri(uri': URL): BuildableRequest ref => 147 | _uri = uri' 148 | this 149 | 150 | fun version(): Version => 151 | """ 152 | The HTTP version as given on the Request Line. 153 | 154 | See: https://tools.ietf.org/html/rfc2616#section-3.1 and https://tools.ietf.org/html/rfc2616#section-5.1 155 | """ 156 | _version 157 | 158 | fun ref set_version(v: Version): BuildableRequest ref => 159 | _version = v 160 | this 161 | 162 | fun header(name: String): (String | None) => 163 | """ 164 | Case insensitive lookup of header value in this request. 165 | Returns `None` if no header with name exists in this request. 166 | """ 167 | _headers.get(name) 168 | 169 | fun headers(): Iterator[Header] => _headers.values() 170 | 171 | fun ref add_header(name: String, value: String): BuildableRequest ref => 172 | """ 173 | Add a header with name and value to this request. 174 | If a header with this name already exists, the given value will be appended to it, 175 | with a separating comma. 176 | """ 177 | // TODO: check for special headers like Transfer-Coding 178 | _headers.add(name, value) 179 | this 180 | 181 | fun ref set_header(name: String, value: String): BuildableRequest ref => 182 | """ 183 | Set a header in this request to the given value. 184 | 185 | If a header with this name already exists, the previous value will be overwritten. 186 | """ 187 | _headers.set(name, value) 188 | this 189 | 190 | fun ref clear_headers(): BuildableRequest ref => 191 | """ 192 | Remove all previously set headers from this request. 193 | """ 194 | _headers.clear() 195 | this 196 | 197 | fun transfer_coding(): (Chunked | None) => 198 | """ 199 | If `Chunked` the request body is encoded with Chunked Transfer-Encoding: 200 | 201 | See: https://tools.ietf.org/html/rfc2616#section-3.6.1 202 | 203 | If `None`, no Transfer-Encoding is applied. A Content-Encoding might be applied 204 | to the body. 205 | """ 206 | _transfer_coding 207 | 208 | fun ref set_transfer_coding(te: (Chunked | None)): BuildableRequest ref => 209 | _transfer_coding = te 210 | this 211 | 212 | fun content_length(): (USize | None) => 213 | """ 214 | The content-length of the body of the request, counted in number of bytes. 215 | 216 | If the content-length is `None`, the request either has no content-length set 217 | or it's transfer-encoding is `Chunked`: https://tools.ietf.org/html/rfc2616#section-3.6.1 218 | """ 219 | _content_length 220 | 221 | fun ref set_content_length(cl: USize): BuildableRequest ref => 222 | _content_length = cl 223 | this 224 | 225 | fun has_body(): Bool => 226 | """ 227 | Returns `true` if either we have Chunked Transfer-Encoding 228 | or a given Content-Length. In those cases we can expect a body. 229 | """ 230 | (transfer_coding() is Chunked) 231 | or 232 | match content_length() 233 | | let x: USize if x > 0 => true 234 | else 235 | false 236 | end 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /http_server/request_ids.pony: -------------------------------------------------------------------------------- 1 | 2 | 3 | type RequestID is USize 4 | 5 | primitive RequestIDs 6 | """ 7 | Utilities for dealing with type RequestID 8 | in order to not assume anything about its actual implementation. 9 | """ 10 | fun max_value(): RequestID => 11 | USize.max_value() 12 | 13 | fun min(id1: RequestID, id2: RequestID): RequestID => 14 | id1.min(id2) 15 | fun max(id1: RequestID, id2: RequestID): RequestID => 16 | id1.max(id2) 17 | 18 | fun next(id: RequestID): RequestID => 19 | id + 1 20 | 21 | fun gt(id1: RequestID, id2: RequestID): Bool => 22 | id1 > id2 23 | 24 | fun gte(id1: RequestID, id2: RequestID): Bool => 25 | id1 >= id2 26 | -------------------------------------------------------------------------------- /http_server/server.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "net" 3 | use "net_ssl" 4 | use "time" 5 | use "debug" 6 | 7 | interface tag _SessionRegistry 8 | be register_session(conn: _ServerConnection) 9 | be unregister_session(conn: _ServerConnection) 10 | 11 | actor Server is _SessionRegistry 12 | """ 13 | Runs an HTTP server. 14 | 15 | ### Server operation 16 | 17 | Information flow into the Server is as follows: 18 | 19 | 1. `Server` listens for incoming TCP connections. 20 | 21 | 2. `_ServerConnHandler` is the notification class for new connections. It creates 22 | a `_ServerConnection` actor and receives all the raw data from TCP. It uses 23 | the `HTTP11RequestParser` to assemble complete `Request` objects which are passed off 24 | to the `_ServerConnection`. 25 | 26 | 3. The `_ServerConnection` actor deals with requests and their bodies 27 | that have been parsed by the `HTTP11RequestParser`. This is where requests get 28 | dispatched to the caller-provided Handler. 29 | """ 30 | let _notify: ServerNotify 31 | var _handler_maker: HandlerFactory val 32 | let _config: ServerConfig 33 | let _sslctx: (SSLContext | None) 34 | let _listen: TCPListener 35 | var _address: NetAddress 36 | let _sessions: SetIs[_ServerConnection tag] = SetIs[_ServerConnection tag] 37 | let _timers: Timers = Timers 38 | var _timer: (Timer tag | None) = None 39 | 40 | new create( 41 | auth: TCPListenAuth, 42 | notify: ServerNotify iso, 43 | handler: HandlerFactory val, 44 | config: ServerConfig, 45 | sslctx: (SSLContext | None) = None) 46 | => 47 | """ 48 | Create a server bound to the given host and service. To do this we 49 | listen for incoming TCP connections, with a notification handler 50 | that will create a server session actor for each one. 51 | """ 52 | _notify = consume notify 53 | _handler_maker = handler 54 | _config = config 55 | _sslctx = sslctx 56 | Debug("starting server with config:\n" + config.to_json()) 57 | 58 | _listen = TCPListener(auth, 59 | _ServerListener(this, config, sslctx, _handler_maker), 60 | config.host, config.port, config.max_concurrent_connections) 61 | 62 | _address = recover NetAddress end 63 | 64 | be register_session(conn: _ServerConnection) => 65 | _sessions.set(conn) 66 | 67 | // only start a timer if we have a connection-timeout configured 68 | if _config.has_timeout() then 69 | match _timer 70 | | None => 71 | let that: Server tag = this 72 | let timeout_interval = _config.timeout_heartbeat_interval 73 | let t = Timer( 74 | object iso is TimerNotify 75 | fun ref apply(timer': Timer, count: U64): Bool => 76 | that._start_heartbeat() 77 | true 78 | end, 79 | Nanos.from_millis(timeout_interval), 80 | Nanos.from_millis(timeout_interval)) 81 | _timer = t 82 | _timers(consume t) 83 | end 84 | end 85 | 86 | be _start_heartbeat() => 87 | // iterate through _sessions and ping all connections 88 | let current_seconds = Time.seconds() // seconds resolution is fine 89 | for session in _sessions.values() do 90 | session._heartbeat(current_seconds) 91 | end 92 | 93 | be unregister_session(conn: _ServerConnection) => 94 | _sessions.unset(conn) 95 | 96 | be set_handler(handler: HandlerFactory val) => 97 | """ 98 | Replace the request handler. 99 | """ 100 | _handler_maker = handler 101 | _listen.set_notify( 102 | _ServerListener(this, _config, _sslctx, _handler_maker)) 103 | 104 | be dispose() => 105 | """ 106 | Shut down the server gracefully. To do this we have to eliminate 107 | any source of further inputs. So we stop listening for new incoming 108 | TCP connections, and close any that still exist. 109 | """ 110 | _listen.dispose() 111 | _timers.dispose() 112 | for conn in _sessions.values() do 113 | conn.dispose() 114 | end 115 | 116 | fun local_address(): NetAddress => 117 | """ 118 | Returns the locally bound address. 119 | """ 120 | _address 121 | 122 | be _listening(address: NetAddress) => 123 | """ 124 | Called when we are listening. 125 | """ 126 | _address = address 127 | _notify.listening(this) 128 | 129 | be _not_listening() => 130 | """ 131 | Called when we fail to listen. 132 | """ 133 | _notify.not_listening(this) 134 | 135 | be _closed() => 136 | """ 137 | Called when we stop listening. 138 | """ 139 | _notify.closed(this) 140 | 141 | -------------------------------------------------------------------------------- /http_server/server_config.pony: -------------------------------------------------------------------------------- 1 | use "time" 2 | use "json" 3 | 4 | class val ServerConfig 5 | 6 | let host: String 7 | """ 8 | Hostname or IP to start listening on. E.g. `localhost` or `127.0.0.1` 9 | 10 | A value of `"0.0.0.0"` will make the server listen on all available interfaces. 11 | 12 | Default: `"localhost"` 13 | """ 14 | 15 | let port: String 16 | """ 17 | Numeric port (e.g. `"80"`) or service name (e.g. `"http"`) 18 | defining the port number to start listening on. 19 | 20 | Chosing `"0"` will let the server start on a random port, chosen by the OS. 21 | 22 | Default: `"0"` 23 | """ 24 | 25 | let connection_timeout: USize 26 | """ 27 | Timeout in seconds after which a connection will be closed. 28 | 29 | Using `0` will make the connection never time out. 30 | 31 | Default: `0` 32 | """ 33 | 34 | let max_request_handling_lag: USize 35 | """ 36 | Maximum number of requests that will be kept without a response generated 37 | before the connection is muted. 38 | 39 | Default: `100` 40 | """ 41 | 42 | let max_concurrent_connections: USize 43 | """ 44 | maximum number of concurrent TCP connections. 45 | Set to `0` to accept unlimited concurrent connections. 46 | 47 | Default: `0` 48 | """ 49 | 50 | let timeout_heartbeat_interval: U64 51 | """ 52 | Interval between heartbeat calls to all tcp connection 53 | in milliseconds 54 | the server keeps track of for them in order to determine 55 | if they should time out. 56 | 57 | Default: `( * 1000) / 4` 58 | """ 59 | 60 | new val create( 61 | host': String = "localhost", 62 | port': String = "0", 63 | connection_timeout': USize = 0, 64 | max_request_handling_lag': USize = 100, 65 | max_concurrent_connections': USize = 0, 66 | timeout_heartbeat_interval': (U64 | None) = None 67 | ) => 68 | host = host' 69 | port = port' 70 | connection_timeout = connection_timeout' 71 | max_request_handling_lag = max_request_handling_lag' 72 | max_concurrent_connections = max_concurrent_connections' 73 | timeout_heartbeat_interval = 74 | match timeout_heartbeat_interval' 75 | | None => 76 | // use a quarter of the actual configured timeout 77 | // but at minimum 1 second 78 | ((connection_timeout.u64() * 1000) / 4).max(1000) 79 | | let interval: U64 => interval 80 | end 81 | 82 | 83 | fun box has_timeout(): Bool => 84 | connection_timeout > 0 85 | 86 | fun box to_json(): String => 87 | let doc = JsonDoc 88 | let obj = JsonObject 89 | obj.data("host") = host 90 | obj.data("port") = port 91 | obj.data("connection_timeout") = connection_timeout.i64() 92 | obj.data("max_request_handling_lag") = max_request_handling_lag.i64() 93 | obj.data("max_concurrent_connections") = max_concurrent_connections.i64() 94 | obj.data("timeout_heartbeat_interval") = timeout_heartbeat_interval.i64() 95 | doc.data = obj 96 | doc.string(where indent = " ", pretty_print = true) 97 | -------------------------------------------------------------------------------- /http_server/server_notify.pony: -------------------------------------------------------------------------------- 1 | interface ServerNotify 2 | """ 3 | Notifications about the creation and closing of `TCPConnection`s 4 | within HTTP servers. 5 | """ 6 | fun ref listening(server: Server ref) => 7 | """ 8 | Called when we are listening. 9 | """ 10 | None 11 | 12 | fun ref not_listening(server: Server ref) => 13 | """ 14 | Called when we fail to listen. 15 | """ 16 | None 17 | 18 | fun ref closed(server: Server ref) => 19 | """ 20 | Called when we stop listening. 21 | """ 22 | None 23 | -------------------------------------------------------------------------------- /http_server/session.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "valbytes" 3 | 4 | trait tag Session 5 | """ 6 | An HTTP Session is the external API to the communication link 7 | between client and server. 8 | 9 | Every request is executed as part of a HTTP Session. 10 | An HTTP Session lives as long as the underlying TCP connection and receives 11 | request data from it and writes response data to it. 12 | 13 | Receiving data and parsing this data into [Request](http_server-Request.md)s is happening on 14 | the TCPConnection actor. The [Session](http_server-Session.md) actor is started when a new TCPConnection 15 | is accepted, and shut down, when the connection is closed. 16 | 17 | ### Receiving a Request 18 | 19 | As part of the Request-Response handling internal to this HTTP library, 20 | a Session is instantiated that forwards requests to a [Handler](http_server-Handler.md), 21 | to actual application code, which in turn sends Responses back to the Session instance 22 | it was instantiated with (See [HandlerFactory](http_server-HandlerFactory.md). 23 | 24 | See [Handler](http_server-Handler.md) on how requests are received by application code. 25 | 26 | ### Sending a Response 27 | 28 | 29 | """ 30 | //////////////////////// 31 | // API THAT CALLS YOU // 32 | //////////////////////// 33 | be _receive_start(request: Request val, request_id: RequestID) => 34 | """ 35 | Start receiving a request. 36 | 37 | This will be called when all headers of an incoming request have been parsed. 38 | [Request](http_server-Request.md) contains all information extracted from 39 | these parts. 40 | 41 | The [RequestID](http_server-RequestID.md) is passed in order for the Session 42 | implementation to maintain the correct request order in case of HTTP pipelining. 43 | Response handling can happen asynchronously at arbitrary times, so the RequestID 44 | helps us to get the responses back into the right order, no matter how they 45 | are received from the application. 46 | """ 47 | None 48 | 49 | be _receive_chunk(data: Array[U8] val, request_id: RequestID) => 50 | """ 51 | Receive a chunk of body data for the request identified by `request_id`. 52 | 53 | The body is split up into arbitrarily sized data chunks, whose size is determined by the 54 | underlying protocol mechanisms, not the actual body size. 55 | """ 56 | None 57 | 58 | be _receive_finished(request_id: RequestID) => 59 | """ 60 | Indicate that the current inbound request, including the body, has been fully received. 61 | """ 62 | None 63 | 64 | be _receive_failed(parse_error: RequestParseError, request_id: RequestID) => 65 | """ 66 | Nofitcation if the request parser failed to parse incoming data as Request. 67 | 68 | Ignored by default. 69 | """ 70 | None 71 | 72 | /////////////////////// 73 | // API THAT YOU CALL // 74 | /////////////////////// 75 | 76 | 77 | // verbose api 78 | be send_start(respone: Response val, request_id: RequestID) => 79 | """ 80 | ### Verbose API 81 | 82 | Start sending a response, submitting the Response status and headers. 83 | 84 | Sending a response via the verbose API needs to be done in 2 or more steps: 85 | 86 | * Session.send_start - exactly once - submit status and headers 87 | * Session.send_chunk - 0 or more times - submit body 88 | * Session.send_finished - exactly once - clean up resources 89 | """ 90 | None 91 | 92 | be send_chunk(data: ByteSeq val, request_id: RequestID) => 93 | """ 94 | ### Verbose API 95 | 96 | Send a piece of body data of the request identified by `request_id`. 97 | This might be the whole body or just a piece of it. 98 | 99 | Notify the Session that the body has been fully sent, by calling `Session.send_finished`. 100 | """ 101 | None 102 | 103 | be send_finished(request_id: RequestID) => 104 | """ 105 | ### Verbose API 106 | 107 | Indicate that the response for `request_id` has been completed, 108 | that is, its status, headers and body have been sent. 109 | 110 | This will clean up resources on the session and 111 | might send pending pipelined responses in addition to this response. 112 | 113 | If this behaviour isnt called, the server might misbehave, especially 114 | with clients doing HTTP pipelining. 115 | """ 116 | None 117 | 118 | be send_cancel(request_id: RequestID) => 119 | """ 120 | Cancel sending an in-flight response. 121 | As the Session will be invalid afterwards, as the response might not have been sent completely, 122 | it is best to close the session afterwards using `Session.dispose()`. 123 | """ 124 | None 125 | 126 | // simple api 127 | be send_no_body(response: Response val, request_id: RequestID) => 128 | """ 129 | ### Simple API 130 | 131 | Send a bodyless Response in one call. 132 | 133 | This call will do all the work of sending the response and cleaning up resources. 134 | No need to call `Session.send_finished()` anymore for this request. 135 | """ 136 | None 137 | 138 | be send(response: Response val, body: ByteArrays, request_id: RequestID) => 139 | """ 140 | ### Simple API 141 | 142 | Send an Response with a body in one call. 143 | 144 | The body must be a [ByteArrays](valbytes-ByteArrays.md) instance. 145 | 146 | Example: 147 | 148 | ```pony 149 | // ... 150 | var bytes = ByteArrays 151 | bytes = bytes + "first line" + "\n" 152 | bytes = bytes + "second line" + "\n" 153 | bytes = bytes + "third line" 154 | 155 | session.send(response, bytes, request_id) 156 | // ... 157 | ``` 158 | 159 | This call will do all the work of sending the response and cleaning up resources. 160 | No need to call `Session.send_finished()` anymore for this request. 161 | """ 162 | None 163 | 164 | // optimized raw api 165 | be send_raw(raw: ByteSeqIter, request_id: RequestID, close_session: Bool = false) => 166 | """ 167 | ### Optimized raw API 168 | 169 | Send raw bytes to the Session in form of a [ByteSeqIter](builtin-ByteSeqIter.md). 170 | 171 | These bytes may or may not include the response body. 172 | You can use `Session.send_chunk()` to send the response body piece by piece. 173 | 174 | If the session should be closed after sending this response, 175 | no matter the requested standard HTTP connection handling, 176 | set `close_session` to `true`. To be a good HTTP citizen, include 177 | a `Connection: close` header in the raw response, to signal to the client 178 | to also close the session. 179 | If set to `false`, then normal HTTP connection handling applies 180 | (request `Connection` header, HTTP/1.0 without `Connection: keep-alive`, etc.). 181 | 182 | To finish sending the response, it is required to call `Session.send_finished()` 183 | to wrap things up, otherwise the server might misbehave. 184 | 185 | This API uses the [TCPConnection.writev](net-TCPConnection.md#writev) method to 186 | optimize putting the given bytes out to the wire. 187 | 188 | To make this optimized path more usable, this library provides the [ResponseBuilder](http_server-ResponseBuilder.md), 189 | which builds up a response into a [ByteSeqIter](builtin-ByteSeqIter.md), thus taylored towards 190 | being used with this API. 191 | 192 | Example: 193 | 194 | ```pony 195 | class MyHandler is Handler 196 | let _session: Session 197 | 198 | new create(session: Session) => 199 | _session = session 200 | 201 | fun ref apply(request: Request val, request_id: RequestID): Any => 202 | let body = 203 | match request.content_length() 204 | | let cl: USize => 205 | "You've sent us " + cl.string() + " bytes! Thank you!" 206 | | None if request.transfer_coding() is Chunked => 207 | "You've sent us some chunks! That's cool!" 208 | | None => 209 | "Dunno how much you've sent us. Probably nothing. That's alright." 210 | end 211 | 212 | _session.send_raw( 213 | Responses.builder() 214 | .set_status(StatusOK) 215 | .add_header("Content-Type", "text/plain; charset=UTF-8") 216 | .add_header("Content-Length", body.size().string()) 217 | .finish_headers() 218 | .add_chunk(body) 219 | .build(), 220 | request_id 221 | ) 222 | // never forget !!! 223 | _session.send_finished(request_id) 224 | ``` 225 | """ 226 | None 227 | 228 | be dispose() => 229 | """ 230 | Close the connection from this end. 231 | """ 232 | None 233 | 234 | be _mute() => 235 | """ 236 | Stop delivering *incoming* data to the handler. This may not 237 | be effective instantly. 238 | """ 239 | None 240 | 241 | be _unmute() => 242 | """ 243 | Resume delivering incoming data to the handler. 244 | """ 245 | None 246 | 247 | be upgrade_protocol(notify: TCPConnectionNotify iso) => 248 | """ 249 | Upgrade this TCP connection to another handler, serving another protocol (e.g. [WebSocket](https://www.rfc-editor.org/rfc/rfc6455.html)). 250 | 251 | Note that this method does not send an HTTP Response with a status of 101. This needs to be done before calling this behaviour. Also, the passed in `notify` will not have its methods [accepted](https://stdlib.ponylang.io/net-TCPConnectionNotify/#connected) or [connected](https://stdlib.ponylang.io/net-TCPConnectionNotify/#connected) called, as the connection is already established. 252 | 253 | After calling this behaviour, this session and the connected [Handler](http_server-Handler.md) instance will not be called again, so it is necessary to do any required clean up right after this call. 254 | 255 | See: 256 | 257 | - [Protocol Upgrade Mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) 258 | - [Upgrade Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade) 259 | """ 260 | None 261 | 262 | 263 | -------------------------------------------------------------------------------- /http_server/status.pony: -------------------------------------------------------------------------------- 1 | interface val Status 2 | """ 3 | HTTP status code. 4 | 5 | See: https://tools.ietf.org/html/rfc2616#section-10 6 | """ 7 | fun apply(): U16 8 | """ 9 | Get the status code as number. 10 | """ 11 | fun string(): String 12 | """ 13 | Get the status code as string including Status-Code and Reason-Phrase 14 | as it usually appears in the response status line: https://tools.ietf.org/html/rfc2616#section-6.1 15 | """ 16 | 17 | primitive StatusContinue is Status 18 | fun apply(): U16 => 100 19 | fun string(): String => "100 Continue" 20 | primitive StatusSwitchingProtocols is Status 21 | fun apply(): U16 => 101 22 | fun string(): String => "101 Switching Protocols" 23 | 24 | primitive StatusOK is Status 25 | fun apply(): U16 => 200 26 | fun string(): String => "200 OK" 27 | primitive StatusCreated is Status 28 | fun apply(): U16 => 201 29 | fun string(): String => "201 Created" 30 | primitive StatusAccepted is Status 31 | fun apply(): U16 => 202 32 | fun string(): String => "202 Accepted" 33 | primitive StatusNonAuthoritativeInfo is Status 34 | fun apply(): U16 => 203 35 | fun string(): String => "203 Non-Authoritative Information" 36 | primitive StatusNoContent is Status 37 | fun apply(): U16 => 204 38 | fun string(): String => "204 No Content" 39 | primitive StatusResetContent is Status 40 | fun apply(): U16 => 205 41 | fun string(): String => "205 Reset Content" 42 | primitive StatusPartialContent is Status 43 | fun apply(): U16 => 206 44 | fun string(): String => "206 Partial Content" 45 | 46 | primitive StatusMultipleChoices is Status 47 | fun apply(): U16 => 300 48 | fun string(): String => "300 Multiple Choices" 49 | primitive StatusMovedPermanently is Status 50 | fun apply(): U16 => 301 51 | fun string(): String => "301 Moved Permanently" 52 | primitive StatusFound is Status 53 | fun apply(): U16 => 302 54 | fun string(): String => "302 Found" 55 | primitive StatusSeeOther is Status 56 | fun apply(): U16 => 303 57 | fun string(): String => "303 See Other" 58 | primitive StatusNotModified is Status 59 | fun apply(): U16 => 304 60 | fun string(): String => "304 Not Modified" 61 | primitive StatusUseProxy is Status 62 | fun apply(): U16 => 305 63 | fun string(): String => "305 Use Proxy" 64 | primitive StatusTemporaryRedirect is Status 65 | fun apply(): U16 => 307 66 | fun string(): String => "307 Temporary Redirect" 67 | 68 | primitive StatusBadRequest is Status 69 | fun apply(): U16 => 400 70 | fun string(): String => "400 Bad Request" 71 | primitive StatusUnauthorized is Status 72 | fun apply(): U16 => 401 73 | fun string(): String => "401 Unauthorized" 74 | primitive StatusPaymentRequired is Status 75 | fun apply(): U16 => 402 76 | fun string(): String => "402 Payment Required" 77 | primitive StatusForbidden is Status 78 | fun apply(): U16 => 403 79 | fun string(): String => "403 Forbidden" 80 | primitive StatusNotFound is Status 81 | fun apply(): U16 => 404 82 | fun string(): String => "404 Not Found" 83 | primitive StatusMethodNotAllowed is Status 84 | fun apply(): U16 => 405 85 | fun string(): String => "405 Method Not Allowed" 86 | primitive StatusNotAcceptable is Status 87 | fun apply(): U16 => 406 88 | fun string(): String => "406 Not Acceptable" 89 | primitive StatusProxyAuthRequired is Status 90 | fun apply(): U16 => 407 91 | fun string(): String => "407 Proxy Authentication Required" 92 | primitive StatusRequestTimeout is Status 93 | fun apply(): U16 => 408 94 | fun string(): String => "408 Request Timeout" 95 | primitive StatusConflict is Status 96 | fun apply(): U16 => 409 97 | fun string(): String => "409 Conflict" 98 | primitive StatusGone is Status 99 | fun apply(): U16 => 410 100 | fun string(): String => "410 Gone" 101 | primitive StatusLengthRequired is Status 102 | fun apply(): U16 => 411 103 | fun string(): String => "411 Length Required" 104 | primitive StatusPreconditionFailed is Status 105 | fun apply(): U16 => 412 106 | fun string(): String => "412 Precondition Failed" 107 | primitive StatusRequestEntityTooLarge is Status 108 | fun apply(): U16 => 413 109 | fun string(): String => "413 Request Entity Too Large" 110 | primitive StatusRequestURITooLong is Status 111 | fun apply(): U16 => 414 112 | fun string(): String => "414 Request URI Too Long" 113 | primitive StatusUnsupportedMediaType is Status 114 | fun apply(): U16 => 415 115 | fun string(): String => "415 Unsupported Media Type" 116 | primitive StatusRequestedRangeNotSatisfiable is Status 117 | fun apply(): U16 => 416 118 | fun string(): String => "416 Requested Range Not Satisfiable" 119 | primitive StatusExpectationFailed is Status 120 | fun apply(): U16 => 417 121 | fun string(): String => "417 Expectation Failed" 122 | primitive StatusTeapot is Status 123 | fun apply(): U16 => 418 124 | fun string(): String => "418 I'm a teapot" 125 | primitive StatusPreconditionRequired is Status 126 | fun apply(): U16 => 428 127 | fun string(): String => "428 Precondition Required" 128 | primitive StatusTooManyRequests is Status 129 | fun apply(): U16 => 429 130 | fun string(): String => "429 Too Many Requests" 131 | primitive StatusRequestHeaderFieldsTooLarge is Status 132 | fun apply(): U16 => 431 133 | fun string(): String => "431 Request Header Fields Too Large" 134 | primitive StatusUnavailableForLegalReasons is Status 135 | fun apply(): U16 => 451 136 | fun string(): String => "451 Unavailable For Legal Reasons" 137 | 138 | primitive StatusInternalServerError is Status 139 | fun apply(): U16 => 500 140 | fun string(): String => "500 Internal Server Error" 141 | primitive StatusNotImplemented is Status 142 | fun apply(): U16 => 501 143 | fun string(): String => "501 Not Implemented" 144 | primitive StatusBadGateway is Status 145 | fun apply(): U16 => 502 146 | fun string(): String => "502 Bad Gateway" 147 | primitive StatusServiceUnavailable is Status 148 | fun apply(): U16 => 503 149 | fun string(): String => "503 Service Unavailable" 150 | primitive StatusGatewayTimeout is Status 151 | fun apply(): U16 => 504 152 | fun string(): String => "504 Gateway Timeout" 153 | primitive StatusHTTPVersionNotSupported is Status 154 | fun apply(): U16 => 505 155 | fun string(): String => "505 HTTP Version Not Supported" 156 | primitive StatusNetworkAuthenticationRequired is Status 157 | fun apply(): U16 => 511 158 | fun string(): String => "511 Network Authentication Required" 159 | -------------------------------------------------------------------------------- /http_server/sync_handler.pony: -------------------------------------------------------------------------------- 1 | use "valbytes" 2 | use "debug" 3 | 4 | interface SyncHandler 5 | """ 6 | Use this handler, when you want to handle your requests without accessing other actors. 7 | """ 8 | fun ref apply(request: Request val, body: (ByteArrays | None)): ByteSeqIter ? 9 | 10 | fun error_response(request: Request): (ByteSeqIter | None) => None 11 | 12 | class SyncHandlerWrapper is Handler 13 | let _session: Session 14 | let _handler: SyncHandler 15 | var _request_id: (RequestID | None) = None 16 | var _request: Request = BuildableRequest.create() 17 | var _body_buffer: ByteArrays = ByteArrays 18 | 19 | var _sent: Bool = false 20 | 21 | new create(session: Session, handler: SyncHandler) => 22 | _handler = handler 23 | _session = session 24 | 25 | fun ref apply(request: Request val, request_id: RequestID) => 26 | _request_id = request_id 27 | _request = request 28 | _sent = false 29 | 30 | if not request.has_body() then 31 | _sent = true 32 | let res = _run_handler(request, None) 33 | _session.send_raw(res, request_id) 34 | end 35 | 36 | fun ref _run_handler(request: Request, body: (ByteArrays | None) = None): ByteSeqIter => 37 | try 38 | _handler(request, None)? 39 | else 40 | // create 500 response 41 | match _handler.error_response(request) 42 | | let bsi: ByteSeqIter => bsi 43 | | None => 44 | // default 500 response 45 | let message = "Internal Server Error" 46 | Responses 47 | .builder(request.version()) 48 | .set_status(StatusInternalServerError) 49 | .add_header("Content-Length", message.size().string()) 50 | .add_header("Content-Type", "text/plain") 51 | .finish_headers() 52 | .add_chunk(message.array()) 53 | .build() 54 | end 55 | end 56 | 57 | fun ref chunk(data: ByteSeq val, request_id: RequestID) => 58 | _body_buffer = _body_buffer + data 59 | 60 | fun ref finished(request_id: RequestID) => 61 | if not _sent then 62 | // resetting _body_buffer 63 | let res = _run_handler(_request, _body_buffer = ByteArrays) 64 | _session.send_raw(res, request_id) 65 | end 66 | _session.send_finished(request_id) 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /http_server/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_server/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 | --------------------------------------------------------------------------------