├── .ackrc ├── .credo.exs ├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ ├── elixir.yml │ └── rerun.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── ex_doc_logo.png ├── logo_source.pxm ├── readme_logo-white.png ├── readme_logo.png └── sticker_flattened.png ├── examples ├── daytime.ex ├── echo.ex ├── http_hello_world.ex └── messenger.ex ├── lib ├── thousand_island.ex └── thousand_island │ ├── acceptor.ex │ ├── acceptor_pool_supervisor.ex │ ├── acceptor_supervisor.ex │ ├── connection.ex │ ├── handler.ex │ ├── listener.ex │ ├── logger.ex │ ├── server.ex │ ├── server_config.ex │ ├── shutdown_listener.ex │ ├── socket.ex │ ├── telemetry.ex │ ├── transport.ex │ └── transports │ ├── ssl.ex │ └── tcp.ex ├── mix.exs ├── mix.lock └── test ├── support ├── ca.pem ├── cert.pem ├── key.pem ├── sendfile └── telemetry_helpers.ex ├── test_helper.exs └── thousand_island ├── handler_test.exs ├── listener_test.exs ├── server_test.exs └── socket_test.exs /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-file=is:report_file_test.xml 2 | --ignore-dir=is:deps 3 | --ignore-dir=is:log 4 | --ignore-dir=is:doc 5 | --ignore-dir=is:.elixir_ls 6 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | # 3 | configs: [ 4 | %{ 5 | name: "default", 6 | strict: true, 7 | checks: %{ 8 | enabled: [ 9 | # 10 | ## Consistency Checks 11 | # 12 | {Credo.Check.Consistency.ExceptionNames, []}, 13 | {Credo.Check.Consistency.LineEndings, []}, 14 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 15 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 16 | {Credo.Check.Consistency.SpaceInParentheses, []}, 17 | {Credo.Check.Consistency.TabsOrSpaces, []}, 18 | 19 | # 20 | ## Design Checks 21 | # 22 | {Credo.Check.Design.AliasUsage, false}, 23 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 24 | {Credo.Check.Design.TagFIXME, []}, 25 | 26 | # 27 | ## Readability Checks 28 | # 29 | {Credo.Check.Readability.AliasOrder, []}, 30 | {Credo.Check.Readability.FunctionNames, []}, 31 | {Credo.Check.Readability.LargeNumbers, []}, 32 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 33 | {Credo.Check.Readability.ModuleAttributeNames, []}, 34 | {Credo.Check.Readability.ModuleDoc, []}, 35 | {Credo.Check.Readability.ModuleNames, []}, 36 | {Credo.Check.Readability.ParenthesesInCondition, []}, 37 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 38 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 39 | {Credo.Check.Readability.PredicateFunctionNames, []}, 40 | {Credo.Check.Readability.PreferImplicitTry, []}, 41 | {Credo.Check.Readability.RedundantBlankLines, []}, 42 | {Credo.Check.Readability.Semicolons, []}, 43 | {Credo.Check.Readability.SpaceAfterCommas, []}, 44 | {Credo.Check.Readability.StringSigils, []}, 45 | {Credo.Check.Readability.TrailingBlankLine, []}, 46 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 47 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 48 | {Credo.Check.Readability.VariableNames, []}, 49 | {Credo.Check.Readability.WithSingleClause, []}, 50 | 51 | # 52 | ## Refactoring Opportunities 53 | # 54 | {Credo.Check.Refactor.Apply, []}, 55 | {Credo.Check.Refactor.CondStatements, []}, 56 | {Credo.Check.Refactor.CyclomaticComplexity, [max_complexity: 15]}, 57 | {Credo.Check.Refactor.FunctionArity, []}, 58 | {Credo.Check.Refactor.MatchInCondition, []}, 59 | {Credo.Check.Refactor.MapJoin, []}, 60 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 61 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 62 | {Credo.Check.Refactor.Nesting, [max_nesting: 3]}, 63 | {Credo.Check.Refactor.UnlessWithElse, []}, 64 | {Credo.Check.Refactor.WithClauses, []}, 65 | {Credo.Check.Refactor.FilterCount, []}, 66 | {Credo.Check.Refactor.FilterFilter, []}, 67 | {Credo.Check.Refactor.RejectReject, []}, 68 | 69 | # 70 | ## Warnings 71 | # 72 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 73 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 74 | {Credo.Check.Warning.Dbg, []}, 75 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 76 | {Credo.Check.Warning.IExPry, []}, 77 | {Credo.Check.Warning.IoInspect, []}, 78 | {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, 79 | {Credo.Check.Warning.OperationOnSameValues, []}, 80 | {Credo.Check.Warning.OperationWithConstantResult, []}, 81 | {Credo.Check.Warning.RaiseInsideRescue, []}, 82 | {Credo.Check.Warning.SpecWithStruct, []}, 83 | {Credo.Check.Warning.WrongTestFileExtension, []}, 84 | {Credo.Check.Warning.UnusedEnumOperation, []}, 85 | {Credo.Check.Warning.UnusedFileOperation, []}, 86 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 87 | {Credo.Check.Warning.UnusedListOperation, []}, 88 | {Credo.Check.Warning.UnusedPathOperation, []}, 89 | {Credo.Check.Warning.UnusedRegexOperation, []}, 90 | {Credo.Check.Warning.UnusedStringOperation, []}, 91 | {Credo.Check.Warning.UnusedTupleOperation, []}, 92 | {Credo.Check.Warning.UnsafeExec, []} 93 | ], 94 | disabled: [ 95 | # 96 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 97 | # and be sure to use `mix credo --strict` to see low priority checks) 98 | # 99 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 100 | {Credo.Check.Consistency.UnusedVariableNames, []}, 101 | {Credo.Check.Design.DuplicatedCode, []}, 102 | {Credo.Check.Design.SkipTestWithoutComment, []}, 103 | {Credo.Check.Readability.AliasAs, []}, 104 | {Credo.Check.Readability.BlockPipe, []}, 105 | {Credo.Check.Readability.ImplTrue, []}, 106 | {Credo.Check.Readability.MultiAlias, []}, 107 | {Credo.Check.Readability.NestedFunctionCalls, []}, 108 | {Credo.Check.Readability.OneArityFunctionInPipe, []}, 109 | {Credo.Check.Readability.SeparateAliasRequire, []}, 110 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 111 | {Credo.Check.Readability.SinglePipe, []}, 112 | {Credo.Check.Readability.Specs, []}, 113 | {Credo.Check.Readability.StrictModuleLayout, []}, 114 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 115 | {Credo.Check.Readability.OnePipePerLine, []}, 116 | {Credo.Check.Refactor.ABCSize, []}, 117 | {Credo.Check.Refactor.AppendSingleItem, []}, 118 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 119 | {Credo.Check.Refactor.FilterReject, []}, 120 | {Credo.Check.Refactor.IoPuts, []}, 121 | {Credo.Check.Refactor.MapMap, []}, 122 | {Credo.Check.Refactor.ModuleDependencies, []}, 123 | {Credo.Check.Refactor.NegatedIsNil, []}, 124 | {Credo.Check.Refactor.PassAsyncInTestCases, []}, 125 | {Credo.Check.Refactor.PipeChainStart, []}, 126 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 127 | {Credo.Check.Refactor.RejectFilter, []}, 128 | {Credo.Check.Refactor.VariableRebinding, []}, 129 | {Credo.Check.Warning.LazyLogging, []}, 130 | {Credo.Check.Warning.LeakyEnvironment, []}, 131 | {Credo.Check.Warning.MapGetUnsafePass, []}, 132 | {Credo.Check.Warning.MixEnv, []}, 133 | {Credo.Check.Warning.UnsafeToAtom, []} 134 | 135 | # {Credo.Check.Refactor.MapInto, []}, 136 | ] 137 | } 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | [ 2 | {"test/support/telemetry_collector.ex", :unmatched_return} 3 | ] 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "mix" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | uses: mtrudel/elixir-ci-actions/.github/workflows/test.yml@main 12 | lint: 13 | uses: mtrudel/elixir-ci-actions/.github/workflows/lint.yml@main 14 | re-run: 15 | needs: [test, lint] 16 | if: failure() && fromJSON(github.run_attempt) < 3 17 | runs-on: ubuntu-latest 18 | steps: 19 | - env: 20 | GH_REPO: ${{ github.repository }} 21 | GH_TOKEN: ${{ github.token }} 22 | GH_DEBUG: api 23 | run: gh workflow run rerun.yml -F run_id=${{ github.run_id }} 24 | -------------------------------------------------------------------------------- /.github/workflows/rerun.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | run_id: 5 | required: true 6 | jobs: 7 | rerun: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: rerun ${{ inputs.run_id }} 11 | env: 12 | GH_REPO: ${{ github.repository }} 13 | GH_TOKEN: ${{ github.token }} 14 | GH_DEBUG: api 15 | run: | 16 | gh run watch ${{ inputs.run_id }} > /dev/null 2>&1 17 | gh run rerun ${{ inputs.run_id }} --failed 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | thousand_island-*.tar 24 | 25 | # Ignore dialyzer caches 26 | /priv/plts/ 27 | 28 | 29 | # ElixirLS 30 | /.elixir_ls -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.14 (25 May 2025) 2 | 3 | ### Enhancements 4 | 5 | * Add support for `ThousandIsland.Socket.connection_information/1` to get info 6 | about the underlying SSL connection, if one is present. 7 | 8 | ## 1.3.13 (29 Apr 2025) 9 | 10 | ### Enhancements 11 | 12 | * Allow `handle_connection` and `handle_data` callbacks to return tuples to call 13 | `handle_continue` per GenServer conventions (#166) 14 | 15 | ## 1.3.12 (21 Mar 2025) 16 | 17 | ### Fixes 18 | 19 | * Do not kill off acceptors when encountering an `:einval` socket status (#162, 20 | thanks @marschro!) 21 | 22 | ## 1.3.11 (23 Feb 2025) 23 | 24 | ### Fixes 25 | 26 | * Properly support `inet_backend` transport option for TCP connections (#155, thanks @paulswartz!) 27 | 28 | ## 1.3.10 (17 Feb 2025) 29 | 30 | ### Enhancements 31 | 32 | * Add support for `certs_keys` TLS config option (#153, thanks @joshk!) 33 | 34 | ## 1.3.9 (6 Jan 2025) 35 | 36 | ### Fixes 37 | 38 | * Explicitly ignore several Handler return values to silence Dialyzer in dependent libraries 39 | 40 | ## 1.3.8 (5 Jan 2025) 41 | 42 | ### Changes 43 | 44 | * Refactor `ThousandIsland.Handler` implementation to facilitate partial reuse 45 | by custom Handler authors (#146 thanks @mruoss!) 46 | 47 | ## 1.3.7 (29 Nov 2024) 48 | 49 | ### Fixes 50 | 51 | * Use socket as returned from transport handshake (#142, thanks @danielspofford!) 52 | 53 | ## 1.3.6 (18 Nov 2024) 54 | 55 | ### Fixes 56 | 57 | * Don't consider remote end closure as an error condition for telemetry 58 | * Fix typo & clarify docs (#117) 59 | * Update security policy (#138) 60 | 61 | ## 1.3.5 (24 Feb 2024) 62 | 63 | ### Fixes 64 | 65 | * Fix regression on non-keyword options (such as `:inet6`) introduced in 1.3.4 66 | (#113, thanks @danschultzer!) 67 | 68 | ## 1.3.4 (23 Feb 2024) 69 | 70 | ### Fixes 71 | 72 | * Fix the ability to override default options (and not override hardcoded 73 | options) for both TCP and SSL transports (#111, thanks @moogle19!) 74 | 75 | ## 1.3.3 (21 Feb 2024) 76 | 77 | ### Changes 78 | 79 | * Do not set `:linger` as a default option on either TCP or SSL transports. This 80 | was causing oddball node hangs on larger instances (#110, thanks @jonjon & 81 | @pojiro!) 82 | 83 | ## 1.3.2 (31 Dec 2023) 84 | 85 | ### Changes 86 | 87 | * Allow `sni_hosts` or `sni_fun` in place of `cert`/`certfile` and 88 | `key`/`keyfile` 89 | 90 | ### Fixes 91 | 92 | * Move `handle_info` fallback clause to module compilation hook (#105) 93 | 94 | ## 1.3.1 (30 Dec 2023) 95 | 96 | ### Fixes 97 | 98 | * Fix downstream dialyzer error introduced by #86 99 | 100 | ## 1.3.0 (30 Dec 2023) 101 | 102 | ### Fixes 103 | 104 | * Fix issue with eventual acceptor starvation when handling certain network 105 | conditions (#103) 106 | 107 | ### Enhancements 108 | 109 | * Add check (and logging) for cases where a Handler implementation does not 110 | return the expected state from GenServer handle_* calls (#96, thanks 111 | @elfenlaid!) 112 | * Allow protocol upgrades within an active connection (#86, thanks @icehaunter!) 113 | 114 | ### Changes 115 | 116 | * Improve file list for hex packaging (#98, thanks @patrickjaberg and 117 | @wojtekmach!) 118 | 119 | ## 1.2.0 (11 Nov 2023) 120 | 121 | ### Enhancements 122 | 123 | * Add `supervisor_options` option to specify top-level supervisor options (#97) 124 | 125 | ## 1.1.0 (23 Oct 2023) 126 | 127 | ### Enhancements 128 | 129 | * Add support for `silent_terminate_on_error` option to run quietly in case of 130 | error (#92) 131 | 132 | ## 1.0.0 (18 Oct 2023) 133 | 134 | ### Changes 135 | 136 | * Do not consider `ECONNABORTED` errors to be exceptional at socket start time 137 | (#72, thanks @dch!) 138 | 139 | ## 1.0.0-pre.7 (12 Aug 2023) 140 | 141 | ### Fixes 142 | 143 | * Fix file handle leak when calling `c:Socket.sendfile` (#78) 144 | 145 | ## 1.0.0-pre.6 (28 Jun 2023) 146 | 147 | ### Enhancements 148 | 149 | * Add support for suspending the acceptance of new requests while continuing to 150 | process existing ones (#70) 151 | 152 | ### Changes 153 | 154 | * Drop Elixir 1.12 as a supported target (it should continue to work, but is no 155 | longer covered by CI) 156 | 157 | ## 1.0.0-pre.5 (16 Jun 2023) 158 | 159 | ### Changes 160 | 161 | * **BREAKING CHANGE** Refactor socket informational functions to mirror the 162 | underlying `:gen_tcp` and `:ssl` APIs (#76, thanks @asakura!). Details: 163 | * Add `ThousandIsland.Socket.sockname/1` 164 | * Add `ThousandIsland.Socket.peername/1` 165 | * Add `ThousandIsland.Socket.peercert/1` 166 | * Remove `ThousandIsland.Socket.local_info/1` 167 | * Remove `ThousandIsland.Socket.peer_info/1` 168 | * Refactor telemetry (#65 thanks @asakura!) 169 | 170 | ## 1.0.0-pre.4 (13 Jun 2023) 171 | 172 | ### Enhancements 173 | 174 | * Improve typespecs (thanks @asakura!) 175 | * Clean up docs & various internal cleanups (thanks @asakura!) 176 | 177 | ## 1.0.0-pre.3 (2 Jun 2023) 178 | 179 | ### Enhancements 180 | 181 | * Total overhaul of typespecs throughout the library (thanks @asakura!) 182 | * Internal refactor of several ancillary functions (thanks @asakura!) 183 | 184 | ## 1.0.0-pre.2 (3 May 2023) 185 | 186 | ### Enhancements 187 | 188 | * Doc improvements 189 | 190 | ## 1.0.0-pre.1 (19 Apr 2023) 191 | 192 | ### Changes 193 | 194 | * None 195 | 196 | ## 0.6.7 (9 Apr 2023) 197 | 198 | ### Changes 199 | 200 | * Thousand Island now sets its `id` field in its spec to be `{ThousandIsland, ref()}` 201 | * `num_connections` defaults to 16384 connections per acceptor 202 | 203 | ### Enhancements 204 | 205 | * Doc improvements 206 | 207 | ## 0.6.6 (7 Apr 2023) 208 | 209 | ### Enhancements 210 | 211 | * Added `num_connections` parameter to specify the max number of concurrent 212 | connections allowed per acceptor 213 | * Added `max_connections_retry_count` and `max_connections_retry_wait` 214 | parameters to configure how Thousand Island behaves when max connections are 215 | reached 216 | * Added `[:thousand_island, :acceptor, :spawn_error]` telemetry event to track 217 | when max connections are reached 218 | * Added max connection logging as part of the 219 | `ThousandIsland.Logger.attach_logger(:error)` level 220 | 221 | ### Changes 222 | 223 | * Refactored connection startup logic to move some burden from acceptor to 224 | connection process 225 | 226 | ## 0.6.5 (27 Mar 2023) 227 | 228 | ### Changes 229 | 230 | * Handshake errors no longer loudly crash the handler process 231 | * `handle_error/3` documentation updated to explicitly mention handshake errors 232 | 233 | ## 0.6.4 (17 Mar 2023) 234 | 235 | ### Changes 236 | 237 | * Modified telemetry event payloads to match the conventions espoused by 238 | `:telemetry.span/3` 239 | 240 | ## 0.6.3 (14 Mar 2023) 241 | 242 | ### Enhancements 243 | 244 | * Added `shutdown_timeout` configuration parameter to configure how long to wait 245 | for existing connections to shutdown before forcibly killing them at shutdown 246 | 247 | ### Changes 248 | 249 | * Default value for `num_acceptors` is now 100 250 | * Default value for `read_timeout` is now 60000 ms 251 | 252 | ### Fixes 253 | 254 | * Added missing documentation for read_timeout configuration parameter 255 | 256 | ## 0.6.2 (22 Feb 2023) 257 | 258 | ### Fixes 259 | 260 | * Fixes a race condition at application shutdown that caused spurious acceptor 261 | crashes to litter the logs (#44). Thanks @danschultzer! 262 | 263 | 264 | ## 0.6.1 (19 Feb 2023) 265 | 266 | ### Changes 267 | 268 | * Expose ThousandIsland.Telemetry.t() as a transparent type 269 | * Pass telemetry errors in metadata rather than metrics 270 | * Allow explicit passing of start times into telemetry spans 271 | 272 | ## 0.6.0 (4 Feb 2023) 273 | 274 | ### Enhancements 275 | 276 | * (Re)introduce telemetry support as specified in the `ThousandIsland.Telemetry` 277 | module 278 | 279 | # Changelog for 0.5.x 280 | 281 | ## 0.5.17 (31 Jan 2023) 282 | 283 | ### Enhancements 284 | 285 | * Add `ThousandIsland.connection_pids/1` to enumerate connection processes 286 | 287 | ## 0.5.16 (21 Jan 2023) 288 | 289 | ### Enhancements 290 | 291 | * Narrow internal pattern matches to enable Handlers to use their own socket calls (#39) 292 | 293 | ### Fixes 294 | 295 | * Fix documentation typos 296 | 297 | ## 0.5.15 (10 Jan 2023) 298 | 299 | ### Enhancements 300 | 301 | * Do not emit GenServer crash logs in connection timeout situations 302 | * Doc updates 303 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | mat@geeky.net. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mat Trudel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Thousand Island](https://github.com/mtrudel/thousand_island/raw/main/assets/readme_logo.png#gh-light-mode-only) 2 | ![Thousand Island](https://github.com/mtrudel/thousand_island/raw/main/assets/readme_logo-white.png#gh-dark-mode-only) 3 | 4 | [![Build Status](https://github.com/mtrudel/thousand_island/workflows/Elixir%20CI/badge.svg)](https://github.com/mtrudel/thousand_island/actions) 5 | [![Docs](https://img.shields.io/badge/api-docs-green.svg?style=flat)](https://hexdocs.pm/thousand_island) 6 | [![Hex.pm](https://img.shields.io/hexpm/v/thousand_island.svg?style=flat&color=blue)](https://hex.pm/packages/thousand_island) 7 | 8 | Thousand Island is a modern, pure Elixir socket server, inspired heavily by 9 | [ranch](https://github.com/ninenines/ranch). It aims to be easy to understand 10 | and reason about, while also being at least as stable and performant as alternatives. 11 | Informal tests place ranch and Thousand Island at roughly the same level of 12 | performance and overhead; short of synthetic scenarios on the busiest of servers, 13 | they perform equally for all intents and purposes. 14 | 15 | Thousand Island is written entirely in Elixir, and is nearly dependency-free (the 16 | only library used is [telemetry](https://github.com/beam-telemetry/telemetry)). 17 | The application strongly embraces OTP design principles, and emphasizes readable, 18 | simple code. The hope is that as much as Thousand Island is capable of backing 19 | the most demanding of services, it is also useful as a simple and approachable 20 | reference for idiomatic OTP design patterns. 21 | 22 | ## Usage 23 | 24 | Thousand Island is implemented as a supervision tree which is intended to be hosted 25 | inside a host application, often as a dependency embedded within a higher-level 26 | protocol library such as [Bandit](https://github.com/mtrudel/bandit). Aside from 27 | supervising the Thousand Island process tree, applications interact with Thousand 28 | Island primarily via the 29 | [`ThousandIsland.Handler`](https://hexdocs.pm/thousand_island/ThousandIsland.Handler.html) behaviour. 30 | 31 | ### Handlers 32 | 33 | The [`ThousandIsland.Handler`](https://hexdocs.pm/thousand_island/ThousandIsland.Handler.html) behaviour defines the interface that Thousand Island 34 | uses to pass [`ThousandIsland.Socket`](https://hexdocs.pm/thousand_island/ThousandIsland.Socket.html)s up to the application level; together they 35 | form the primary interface that most applications will have with Thousand Island. 36 | Thousand Island comes with a few simple protocol handlers to serve as examples; 37 | these can be found in the [examples](https://github.com/mtrudel/thousand_island/tree/main/examples) 38 | folder of this project. A simple implementation would look like this: 39 | 40 | ```elixir 41 | defmodule Echo do 42 | use ThousandIsland.Handler 43 | 44 | @impl ThousandIsland.Handler 45 | def handle_data(data, socket, state) do 46 | ThousandIsland.Socket.send(socket, data) 47 | {:continue, state} 48 | end 49 | end 50 | 51 | {:ok, pid} = ThousandIsland.start_link(port: 1234, handler_module: Echo) 52 | ``` 53 | 54 | For more information, please consult the [`ThousandIsland.Handler`](https://hexdocs.pm/thousand_island/ThousandIsland.Handler.html) documentation. 55 | 56 | ### Starting a Thousand Island Server 57 | 58 | Thousand Island servers exist as a supervision tree, and are started by a call 59 | to 60 | [`ThousandIsland.start_link/1`](https://hexdocs.pm/thousand_island/ThousandIsland.html#start_link/1). 61 | There are a number of options supported; for a complete description, consult the 62 | [Thousand Island 63 | docs](https://hexdocs.pm/thousand_island/ThousandIsland.html#t:options/0). 64 | 65 | ### Connection Draining & Shutdown 66 | 67 | The `ThousandIsland.Server` process is just a standard `Supervisor`, so all the 68 | usual rules regarding shutdown and shutdown timeouts apply. Immediately upon 69 | beginning the shutdown sequence the `ThousandIsland.ShutdownListener` will cause 70 | the listening socket to shut down, which in turn will cause all of the `Acceptor` 71 | processes to shut down as well. At this point all that is left in the supervision 72 | tree are several layers of Supervisors and whatever `Handler` processes were 73 | in progress when shutdown was initiated. At this point, standard Supervisor shutdown 74 | timeout semantics give existing connections a chance to finish things up. `Handler` 75 | processes trap exit, so they continue running beyond shutdown until they either 76 | complete or are `:brutal_kill`ed after their shutdown timeout expires. 77 | 78 | The `shutdown_timeout` configuration option allows for fine grained control of 79 | the shutdown timeout value. It defaults to 15000 ms. 80 | 81 | ### Logging & Telemetry 82 | 83 | As a low-level library, Thousand Island purposely does not do any inline 84 | logging of any kind. The [`ThousandIsland.Logger`](https://hexdocs.pm/thousand_island/ThousandIsland.Logger.html) module defines a number of 85 | functions to aid in tracing connections at various log levels, and such logging 86 | can be dynamically enabled and disabled against an already running server. This 87 | logging is backed by telemetry events internally. 88 | 89 | Thousand Island emits a rich set of telemetry events including spans for each 90 | server, acceptor process, and individual client connection. These telemetry 91 | events are documented in the [`ThousandIsland.Telemetry`](https://hexdocs.pm/thousand_island/ThousandIsland.Telemetry.html) module. 92 | 93 | ## Implementation Notes 94 | 95 | At a top-level, a `Server` coordinates the processes involved in responding to 96 | connections on a socket. A `Server` manages two top-level processes: a `Listener` 97 | which is responsible for actually binding to the port and managing the resultant 98 | listener socket, and an `AcceptorPoolSupervisor` which is responsible for managing 99 | a pool of `AcceptorSupervisor` processes. 100 | 101 | Each `AcceptorSupervisor` process (there are 100 by default) manages two processes: 102 | an `Acceptor` which accepts connections made to the server's listener socket, 103 | and a `DynamicSupervisor` which supervises the processes backing individual 104 | client connections. Every time a client connects to the server's port, one of 105 | the `Acceptor`s receives the connection in the form of a socket. It then creates 106 | a new process based on the configured handler to manage this connection, and 107 | immediately waits for another connection. It is worth noting that `Acceptor` 108 | processes are long-lived, and normally live for the entire period that the 109 | `Server` is running. 110 | 111 | A handler process is tied to the lifecycle of a client connection, and is 112 | only started when a client connects. The length of its lifetime beyond that of the 113 | underlying connection is dependent on the behaviour of the configured Handler module. 114 | In typical cases its lifetime is directly related to that of the underlying connection. 115 | 116 | This hierarchical approach reduces the time connections spend waiting to be accepted, 117 | and also reduces contention for `DynamicSupervisor` access when creating new 118 | `Handler` processes. Each `AcceptorSupervisor` subtree functions nearly 119 | autonomously, improving scalability and crash resiliency. 120 | 121 | Graphically, this shakes out like so: 122 | 123 | ```mermaid 124 | graph TD; 125 | Server(Server: supervisor, rest_for_one)-->Listener; 126 | Server-->AcceptorPoolSupervisor(AcceptorPoolSupervisor: dynamic supervisor); 127 | AcceptorPoolSupervisor--1...n-->AcceptorSupervisor(AcceptorSupervisor: supervisor, rest_for_one) 128 | AcceptorSupervisor-->DynamicSupervisor 129 | AcceptorSupervisor-->Acceptor(Acceptor: task) 130 | DynamicSupervisor--1...n-->Handler(Handler: gen_server) 131 | Server-->ShutdownListener; 132 | ``` 133 | 134 | Thousand Island does not use named processes or other 'global' state internally 135 | (other than telemetry event names). It is completely supported for a single node 136 | to host any number of `Server` processes each listening on a different port. 137 | 138 | ## Contributing 139 | 140 | Contributions to Thousand Island are very much welcome! Before undertaking any substantial work, please 141 | open an issue on the project to discuss ideas and planned approaches so we can ensure we keep 142 | progress moving in the same direction. 143 | 144 | All contributors must agree and adhere to the project's [Code of 145 | Conduct](https://github.com/mtrudel/thousand_island/blob/main/CODE_OF_CONDUCT.md). 146 | 147 | Security disclosures should be handled per Thousand Island's published [security policy](https://github.com/mtrudel/thousand_island/blob/main/SECURITY.md). 148 | 149 | ## Installation 150 | 151 | Thousand Island is [available in Hex](https://hex.pm/packages/thousand_island). The package 152 | can be installed by adding `thousand_island` to your list of dependencies in `mix.exs`: 153 | 154 | ```elixir 155 | def deps do 156 | [ 157 | {:thousand_island, "~> 1.0"} 158 | ] 159 | end 160 | ``` 161 | 162 | Documentation can be found at [https://hexdocs.pm/thousand_island](https://hexdocs.pm/thousand_island). 163 | 164 | ## License 165 | 166 | MIT 167 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Bug and security fixes are provided only for the most recent version of Thousand Island. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Security disclosures should be sent privately to mat@geeky.net. 10 | -------------------------------------------------------------------------------- /assets/ex_doc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrudel/thousand_island/9a534085602fae1d3154088d342d38f216511815/assets/ex_doc_logo.png -------------------------------------------------------------------------------- /assets/logo_source.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrudel/thousand_island/9a534085602fae1d3154088d342d38f216511815/assets/logo_source.pxm -------------------------------------------------------------------------------- /assets/readme_logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrudel/thousand_island/9a534085602fae1d3154088d342d38f216511815/assets/readme_logo-white.png -------------------------------------------------------------------------------- /assets/readme_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrudel/thousand_island/9a534085602fae1d3154088d342d38f216511815/assets/readme_logo.png -------------------------------------------------------------------------------- /assets/sticker_flattened.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrudel/thousand_island/9a534085602fae1d3154088d342d38f216511815/assets/sticker_flattened.png -------------------------------------------------------------------------------- /examples/daytime.ex: -------------------------------------------------------------------------------- 1 | defmodule Daytime do 2 | @moduledoc """ 3 | A sample Handler implementation of the Daytime protocol 4 | 5 | https://en.wikipedia.org/wiki/Daytime_Protocol 6 | """ 7 | 8 | use ThousandIsland.Handler 9 | 10 | @impl ThousandIsland.Handler 11 | def handle_connection(socket, state) do 12 | time = DateTime.utc_now() |> to_string() 13 | ThousandIsland.Socket.send(socket, time) 14 | {:close, state} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /examples/echo.ex: -------------------------------------------------------------------------------- 1 | defmodule Echo do 2 | @moduledoc """ 3 | A sample Handler implementation of the Echo protocol 4 | 5 | https://en.wikipedia.org/wiki/Echo_Protocol 6 | """ 7 | 8 | use ThousandIsland.Handler 9 | 10 | @impl ThousandIsland.Handler 11 | def handle_data(data, socket, state) do 12 | ThousandIsland.Socket.send(socket, data) 13 | {:continue, state} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/http_hello_world.ex: -------------------------------------------------------------------------------- 1 | defmodule HTTPHelloWorld do 2 | @moduledoc """ 3 | A sample Handler implementation of a simple HTTP Server. Intended to be the 4 | simplest thing that can answer a browser request and nothing more. Not even 5 | remotely strictly HTTP compliant. 6 | """ 7 | 8 | use ThousandIsland.Handler 9 | 10 | @impl ThousandIsland.Handler 11 | def handle_data(_data, socket, state) do 12 | ThousandIsland.Socket.send(socket, "HTTP/1.0 200 OK\r\n\r\nHello, World") 13 | {:close, state} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/messenger.ex: -------------------------------------------------------------------------------- 1 | defmodule Messenger do 2 | @moduledoc """ 3 | A sample Handler implementation that allows for simultaneous bidirectional sending 4 | of messages to demonstrate that the Handler process is just a GenServer under the 5 | hood 6 | """ 7 | 8 | use ThousandIsland.Handler 9 | 10 | def send_message(pid, msg) do 11 | GenServer.cast(pid, {:send, msg}) 12 | end 13 | 14 | @impl ThousandIsland.Handler 15 | def handle_connection(socket, state) do 16 | {:ok, {remote_address, _port}} = ThousandIsland.Socket.peername(socket) 17 | IO.puts("#{inspect(self())} received connection from #{remote_address}") 18 | {:continue, state} 19 | end 20 | 21 | @impl ThousandIsland.Handler 22 | def handle_data(msg, _socket, state) do 23 | IO.puts("Received #{msg}") 24 | {:continue, state} 25 | end 26 | 27 | @impl GenServer 28 | def handle_cast({:send, msg}, {socket, state}) do 29 | ThousandIsland.Socket.send(socket, msg) 30 | {:noreply, {socket, state}, socket.read_timeout} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/thousand_island.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland do 2 | @moduledoc """ 3 | Thousand Island is a modern, pure Elixir socket server, inspired heavily by 4 | [ranch](https://github.com/ninenines/ranch). It aims to be easy to understand 5 | & reason about, while also being at least as stable and performant as alternatives. 6 | 7 | Thousand Island is implemented as a supervision tree which is intended to be hosted 8 | inside a host application, often as a dependency embedded within a higher-level 9 | protocol library such as [Bandit](https://github.com/mtrudel/bandit). Aside from 10 | supervising the Thousand Island process tree, applications interact with Thousand 11 | Island primarily via the `ThousandIsland.Handler` behaviour. 12 | 13 | ## Handlers 14 | 15 | The `ThousandIsland.Handler` behaviour defines the interface that Thousand Island 16 | uses to pass `ThousandIsland.Socket`s up to the application level; together they 17 | form the primary interface that most applications will have with Thousand Island. 18 | Thousand Island comes with a few simple protocol handlers to serve as examples; 19 | these can be found in the [examples](https://github.com/mtrudel/thousand_island/tree/main/examples) 20 | folder of this project. A simple implementation would look like this: 21 | 22 | ```elixir 23 | defmodule Echo do 24 | use ThousandIsland.Handler 25 | 26 | @impl ThousandIsland.Handler 27 | def handle_data(data, socket, state) do 28 | ThousandIsland.Socket.send(socket, data) 29 | {:continue, state} 30 | end 31 | end 32 | 33 | {:ok, pid} = ThousandIsland.start_link(port: 1234, handler_module: Echo) 34 | ``` 35 | 36 | For more information, please consult the `ThousandIsland.Handler` documentation. 37 | 38 | ## Starting a Thousand Island Server 39 | 40 | A typical use of `ThousandIsland` might look like the following: 41 | 42 | ```elixir 43 | defmodule MyApp.Supervisor do 44 | # ... other Supervisor boilerplate 45 | 46 | def init(config) do 47 | children = [ 48 | # ... other children as dictated by your app 49 | {ThousandIsland, port: 1234, handler_module: MyApp.ConnectionHandler} 50 | ] 51 | 52 | Supervisor.init(children, strategy: :one_for_one) 53 | end 54 | end 55 | ``` 56 | 57 | You can also start servers directly via the `start_link/1` function: 58 | 59 | ```elixir 60 | {:ok, pid} = ThousandIsland.start_link(port: 1234, handler_module: MyApp.ConnectionHandler) 61 | ``` 62 | 63 | ## Configuration 64 | 65 | A number of options are defined when starting a server. The complete list is 66 | defined by the `t:ThousandIsland.options/0` type. 67 | 68 | ## Connection Draining & Shutdown 69 | 70 | `ThousandIsland` instances are just a process tree consisting of standard 71 | `Supervisor`, `GenServer` and `Task` modules, and so the usual rules regarding 72 | shutdown and shutdown timeouts apply. Immediately upon beginning the shutdown 73 | sequence the ThousandIsland.ShutdownListener process will cause the listening 74 | socket to shut down, which in turn will cause all of the 75 | ThousandIsland.Acceptor processes to shut down as well. At this point all that 76 | is left in the supervision tree are several layers of Supervisors and whatever 77 | `Handler` processes were in progress when shutdown was initiated. At this 78 | point, standard `Supervisor` shutdown timeout semantics give existing 79 | connections a chance to finish things up. `Handler` processes trap exit, so 80 | they continue running beyond shutdown until they either complete or are 81 | `:brutal_kill`ed after their shutdown timeout expires. 82 | 83 | ## Logging & Telemetry 84 | 85 | As a low-level library, Thousand Island purposely does not do any inline 86 | logging of any kind. The `ThousandIsland.Logger` module defines a number of 87 | functions to aid in tracing connections at various log levels, and such logging 88 | can be dynamically enabled and disabled against an already running server. This 89 | logging is backed by telemetry events internally. 90 | 91 | Thousand Island emits a rich set of telemetry events including spans for each 92 | server, acceptor process, and individual client connection. These telemetry 93 | events are documented in the `ThousandIsland.Telemetry` module. 94 | """ 95 | 96 | @typedoc """ 97 | Possible options to configure a server. Valid option values are as follows: 98 | 99 | * `handler_module`: The name of the module used to handle connections to this server. 100 | The module is expected to implement the `ThousandIsland.Handler` behaviour. Required 101 | * `handler_options`: A term which is passed as the initial state value to 102 | `c:ThousandIsland.Handler.handle_connection/2` calls. Optional, defaulting to nil 103 | * `port`: The TCP port number to listen on. If not specified this defaults to 4000. 104 | If a port number of `0` is given, the server will dynamically assign a port number 105 | which can then be obtained via `ThousandIsland.listener_info/1` or 106 | `ThousandIsland.Socket.sockname/1` 107 | * `transport_module`: The name of the module which provides basic socket functions. 108 | Thousand Island provides `ThousandIsland.Transports.TCP` and `ThousandIsland.Transports.SSL`, 109 | which provide clear and TLS encrypted TCP sockets respectively. If not specified this 110 | defaults to `ThousandIsland.Transports.TCP` 111 | * `transport_options`: A keyword list of options to be passed to the transport module's 112 | `c:ThousandIsland.Transport.listen/2` function. Valid values depend on the transport 113 | module specified in `transport_module` and can be found in the documentation for the 114 | `ThousandIsland.Transports.TCP` and `ThousandIsland.Transports.SSL` modules. Any options 115 | in terms of interfaces to listen to / certificates and keys to use for SSL connections 116 | will be passed in via this option 117 | * `genserver_options`: A term which is passed as the option value to the handler module's 118 | underlying `GenServer.start_link/3` call. Optional, defaulting to `[]` 119 | * `supervisor_options`: A term which is passed as the option value to this server's top-level 120 | supervisor's `Supervisor.start_link/3` call. Useful for setting the `name` for this server. 121 | Optional, defaulting to `[]` 122 | * `num_acceptors`: The number of acceptor processes to run. Defaults to 100 123 | * `num_connections`: The maximum number of concurrent connections which each acceptor will 124 | accept before throttling connections. Connections will be throttled by having the acceptor 125 | process wait `max_connections_retry_wait` milliseconds, up to `max_connections_retry_count` 126 | times for existing connections to terminate & make room for this new connection. If there is 127 | still no room for this new connection after this interval, the acceptor will close the client 128 | connection and emit a `[:thousand_island, :acceptor, :spawn_error]` telemetry event. This number 129 | is expressed per-acceptor, so the total number of maximum connections for a Thousand Island 130 | server is `num_acceptors * num_connections`. Defaults to `16_384` 131 | * `max_connections_retry_wait`: How long to wait during each iteration as described in 132 | `num_connectors` above, in milliseconds. Defaults to `1000` 133 | * `max_connections_retry_count`: How many iterations to wait as described in `num_connectors` 134 | above. Defaults to `5` 135 | * `read_timeout`: How long to wait for client data before closing the connection, in 136 | milliseconds. Defaults to 60_000 137 | * `shutdown_timeout`: How long to wait for existing client connections to complete before 138 | forcibly shutting those connections down at server shutdown time, in milliseconds. Defaults to 139 | 15_000. May also be `:infinity` or `:brutal_kill` as described in the `Supervisor` 140 | documentation 141 | * `silent_terminate_on_error`: Whether to silently ignore errors returned by the handler or to 142 | surface them to the runtime via an abnormal termination result. This only applies to errors 143 | returned via `{:error, reason, state}` responses; exceptions raised within a handler are always 144 | logged regardless of this value. Note also that telemetry events will always be sent for errors 145 | regardless of this value. Defaults to false 146 | """ 147 | @type options :: [ 148 | handler_module: module(), 149 | handler_options: term(), 150 | genserver_options: GenServer.options(), 151 | supervisor_options: [Supervisor.option()], 152 | port: :inet.port_number(), 153 | transport_module: module(), 154 | transport_options: transport_options(), 155 | num_acceptors: pos_integer(), 156 | num_connections: non_neg_integer() | :infinity, 157 | max_connections_retry_count: non_neg_integer(), 158 | max_connections_retry_wait: timeout(), 159 | read_timeout: timeout(), 160 | shutdown_timeout: timeout(), 161 | silent_terminate_on_error: boolean() 162 | ] 163 | 164 | @typedoc "A module implementing `ThousandIsland.Transport` behaviour" 165 | @type transport_module :: ThousandIsland.Transports.TCP | ThousandIsland.Transports.SSL 166 | 167 | @typedoc "A keyword list of options to be passed to the transport module's `listen/2` function" 168 | @type transport_options() :: 169 | ThousandIsland.Transports.TCP.options() | ThousandIsland.Transports.SSL.options() 170 | 171 | @doc false 172 | @spec child_spec(options()) :: Supervisor.child_spec() 173 | def child_spec(opts) do 174 | %{ 175 | id: {__MODULE__, make_ref()}, 176 | start: {__MODULE__, :start_link, [opts]}, 177 | type: :supervisor, 178 | restart: :permanent 179 | } 180 | end 181 | 182 | @doc """ 183 | Starts a `ThousandIsland` instance with the given options. Returns a pid 184 | that can be used to further manipulate the server via other functions defined on 185 | this module in the case of success, or an error tuple describing the reason the 186 | server was unable to start in the case of failure. 187 | """ 188 | @spec start_link(options()) :: Supervisor.on_start() 189 | def start_link(opts \\ []) do 190 | opts 191 | |> ThousandIsland.ServerConfig.new() 192 | |> ThousandIsland.Server.start_link() 193 | end 194 | 195 | @doc """ 196 | Returns information about the address and port that the server is listening on 197 | """ 198 | @spec listener_info(Supervisor.supervisor()) :: 199 | {:ok, ThousandIsland.Transport.socket_info()} | :error 200 | def listener_info(supervisor) do 201 | case ThousandIsland.Server.listener_pid(supervisor) do 202 | nil -> :error 203 | pid -> {:ok, ThousandIsland.Listener.listener_info(pid)} 204 | end 205 | end 206 | 207 | @doc """ 208 | Gets a list of active connection processes. This is inherently a bit of a leaky notion in the 209 | face of concurrency, as there may be connections coming and going during the period that this 210 | function takes to run. Callers should account for the possibility that new connections may have 211 | been made since / during this call, and that processes returned by this call may have since 212 | completed. The order that connection processes are returned in is not specified 213 | """ 214 | @spec connection_pids(Supervisor.supervisor()) :: {:ok, [pid()]} | :error 215 | def connection_pids(supervisor) do 216 | case ThousandIsland.Server.acceptor_pool_supervisor_pid(supervisor) do 217 | nil -> :error 218 | acceptor_pool_pid -> {:ok, collect_connection_pids(acceptor_pool_pid)} 219 | end 220 | end 221 | 222 | @doc """ 223 | Suspend the server. This will close the listening port, and will stop the acceptance of new 224 | connections. Existing connections will stay connected and will continue to be processed. 225 | 226 | The server can later be resumed by calling `resume/1`, or shut down via standard supervision 227 | patterns. 228 | 229 | If this function returns `:error`, it is unlikely that the server is in a useable state 230 | 231 | Note that if you do not explicitly set a port (or if you set port to `0`), then the server will 232 | bind to a different port when you resume it. This new port can be obtained as usual via the 233 | `listener_info/1` function. This is not a concern if you explicitly set a port value when first 234 | instantiating the server 235 | """ 236 | defdelegate suspend(supervisor), to: ThousandIsland.Server 237 | 238 | @doc """ 239 | Resume a suspended server. This will reopen the listening port, and resume the acceptance of new 240 | connections 241 | """ 242 | defdelegate resume(supervisor), to: ThousandIsland.Server 243 | 244 | defp collect_connection_pids(acceptor_pool_pid) do 245 | acceptor_pool_pid 246 | |> ThousandIsland.AcceptorPoolSupervisor.acceptor_supervisor_pids() 247 | |> Enum.reduce([], fn acceptor_sup_pid, acc -> 248 | case ThousandIsland.AcceptorSupervisor.connection_sup_pid(acceptor_sup_pid) do 249 | nil -> acc 250 | connection_sup_pid -> connection_pids(connection_sup_pid, acc) 251 | end 252 | end) 253 | end 254 | 255 | defp connection_pids(connection_sup_pid, acc) do 256 | connection_sup_pid 257 | |> DynamicSupervisor.which_children() 258 | |> Enum.reduce(acc, fn 259 | {_, connection_pid, _, _}, acc when is_pid(connection_pid) -> 260 | [connection_pid | acc] 261 | 262 | _, acc -> 263 | acc 264 | end) 265 | end 266 | 267 | @doc """ 268 | Synchronously stops the given server, waiting up to the given number of milliseconds 269 | for existing connections to finish up. Immediately upon calling this function, 270 | the server stops listening for new connections, and then proceeds to wait until 271 | either all existing connections have completed or the specified timeout has 272 | elapsed. 273 | """ 274 | @spec stop(Supervisor.supervisor(), timeout()) :: :ok 275 | def stop(supervisor, connection_wait \\ 15_000) do 276 | Supervisor.stop(supervisor, :normal, connection_wait) 277 | end 278 | end 279 | -------------------------------------------------------------------------------- /lib/thousand_island/acceptor.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Acceptor do 2 | @moduledoc false 3 | 4 | use Task, restart: :transient 5 | 6 | @spec start_link( 7 | {server :: Supervisor.supervisor(), parent :: Supervisor.supervisor(), 8 | ThousandIsland.ServerConfig.t()} 9 | ) :: {:ok, pid()} 10 | def start_link(arg), do: Task.start_link(__MODULE__, :run, [arg]) 11 | 12 | @spec run( 13 | {server :: Supervisor.supervisor(), parent :: Supervisor.supervisor(), 14 | ThousandIsland.ServerConfig.t()} 15 | ) :: no_return 16 | def run({server_pid, parent_pid, %ThousandIsland.ServerConfig{} = server_config}) do 17 | listener_pid = ThousandIsland.Server.listener_pid(server_pid) 18 | {listener_socket, listener_span} = ThousandIsland.Listener.acceptor_info(listener_pid) 19 | connection_sup_pid = ThousandIsland.AcceptorSupervisor.connection_sup_pid(parent_pid) 20 | acceptor_span = ThousandIsland.Telemetry.start_child_span(listener_span, :acceptor) 21 | accept(listener_socket, connection_sup_pid, server_config, acceptor_span, 0) 22 | end 23 | 24 | defp accept(listener_socket, connection_sup_pid, server_config, span, count) do 25 | with {:ok, socket} <- server_config.transport_module.accept(listener_socket), 26 | :ok <- ThousandIsland.Connection.start(connection_sup_pid, socket, server_config, span) do 27 | accept(listener_socket, connection_sup_pid, server_config, span, count + 1) 28 | else 29 | {:error, :too_many_connections} -> 30 | ThousandIsland.Telemetry.span_event(span, :spawn_error) 31 | accept(listener_socket, connection_sup_pid, server_config, span, count + 1) 32 | 33 | {:error, reason} when reason in [:econnaborted, :einval] -> 34 | ThousandIsland.Telemetry.span_event(span, reason) 35 | accept(listener_socket, connection_sup_pid, server_config, span, count + 1) 36 | 37 | {:error, :closed} -> 38 | ThousandIsland.Telemetry.stop_span(span, %{connections: count}) 39 | 40 | {:error, reason} -> 41 | ThousandIsland.Telemetry.stop_span(span, %{connections: count}, %{error: reason}) 42 | raise "Unexpected error in accept: #{inspect(reason)}" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/thousand_island/acceptor_pool_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.AcceptorPoolSupervisor do 2 | @moduledoc false 3 | 4 | use Supervisor 5 | 6 | @spec start_link({server_pid :: pid, ThousandIsland.ServerConfig.t()}) :: Supervisor.on_start() 7 | def start_link(arg) do 8 | Supervisor.start_link(__MODULE__, arg) 9 | end 10 | 11 | @spec acceptor_supervisor_pids(Supervisor.supervisor()) :: [pid()] 12 | def acceptor_supervisor_pids(supervisor) do 13 | supervisor 14 | |> Supervisor.which_children() 15 | |> Enum.reduce([], fn 16 | {_, acceptor_pid, _, _}, acc when is_pid(acceptor_pid) -> [acceptor_pid | acc] 17 | _, acc -> acc 18 | end) 19 | end 20 | 21 | @spec suspend(Supervisor.supervisor()) :: :ok | :error 22 | def suspend(pid) do 23 | pid 24 | |> acceptor_supervisor_pids() 25 | |> Enum.map(&ThousandIsland.AcceptorSupervisor.suspend/1) 26 | |> Enum.all?(&(&1 == :ok)) 27 | |> if(do: :ok, else: :error) 28 | end 29 | 30 | @spec resume(Supervisor.supervisor()) :: :ok | :error 31 | def resume(pid) do 32 | pid 33 | |> acceptor_supervisor_pids() 34 | |> Enum.map(&ThousandIsland.AcceptorSupervisor.resume/1) 35 | |> Enum.all?(&(&1 == :ok)) 36 | |> if(do: :ok, else: :error) 37 | end 38 | 39 | @impl Supervisor 40 | @spec init({server_pid :: pid, ThousandIsland.ServerConfig.t()}) :: 41 | {:ok, 42 | {Supervisor.sup_flags(), 43 | [Supervisor.child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}} 44 | def init({server_pid, %ThousandIsland.ServerConfig{num_acceptors: num_acceptors} = config}) do 45 | base_spec = {ThousandIsland.AcceptorSupervisor, {server_pid, config}} 46 | 47 | 1..num_acceptors 48 | |> Enum.map(&Supervisor.child_spec(base_spec, id: "acceptor-#{&1}")) 49 | |> Supervisor.init(strategy: :one_for_one) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/thousand_island/acceptor_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.AcceptorSupervisor do 2 | @moduledoc false 3 | 4 | use Supervisor 5 | 6 | @spec start_link({server_pid :: pid, ThousandIsland.ServerConfig.t()}) :: Supervisor.on_start() 7 | def start_link(arg) do 8 | Supervisor.start_link(__MODULE__, arg) 9 | end 10 | 11 | @spec connection_sup_pid(Supervisor.supervisor()) :: pid() | nil 12 | def connection_sup_pid(supervisor) do 13 | supervisor 14 | |> Supervisor.which_children() 15 | |> Enum.find_value(fn 16 | {:connection_sup, connection_sup_pid, _, _} when is_pid(connection_sup_pid) -> 17 | connection_sup_pid 18 | 19 | _ -> 20 | false 21 | end) 22 | end 23 | 24 | @spec suspend(Supervisor.supervisor()) :: :ok | :error 25 | def suspend(pid) do 26 | case Supervisor.terminate_child(pid, :acceptor) do 27 | :ok -> :ok 28 | {:error, :not_found} -> :error 29 | end 30 | end 31 | 32 | @spec resume(Supervisor.supervisor()) :: :ok | :error 33 | def resume(pid) do 34 | case Supervisor.restart_child(pid, :acceptor) do 35 | {:ok, _child} -> :ok 36 | {:error, reason} when reason in [:running, :restarting] -> :ok 37 | {:error, _reason} -> :error 38 | end 39 | end 40 | 41 | @impl Supervisor 42 | @spec init({server_pid :: pid, ThousandIsland.ServerConfig.t()}) :: 43 | {:ok, 44 | {Supervisor.sup_flags(), 45 | [Supervisor.child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}} 46 | def init({server_pid, %ThousandIsland.ServerConfig{} = config}) do 47 | children = [ 48 | {DynamicSupervisor, strategy: :one_for_one, max_children: config.num_connections} 49 | |> Supervisor.child_spec(id: :connection_sup), 50 | {ThousandIsland.Acceptor, {server_pid, self(), config}} 51 | |> Supervisor.child_spec(id: :acceptor) 52 | ] 53 | 54 | Supervisor.init(children, strategy: :rest_for_one) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/thousand_island/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Connection do 2 | @moduledoc false 3 | 4 | @spec start( 5 | Supervisor.supervisor(), 6 | ThousandIsland.Transport.socket(), 7 | ThousandIsland.ServerConfig.t(), 8 | ThousandIsland.Telemetry.t() 9 | ) :: 10 | :ignore 11 | | :ok 12 | | {:ok, pid, info :: term} 13 | | {:error, :too_many_connections | {:already_started, pid} | term} 14 | def start(sup_pid, raw_socket, %ThousandIsland.ServerConfig{} = server_config, acceptor_span) do 15 | # This is a multi-step process since we need to do a bit of work from within 16 | # the process which owns the socket (us, at this point). 17 | 18 | # First, capture the start time for telemetry purposes 19 | start_time = ThousandIsland.Telemetry.monotonic_time() 20 | 21 | # Start by defining the worker process which will eventually handle this socket 22 | child_spec = 23 | {server_config.handler_module, 24 | {server_config.handler_options, server_config.genserver_options}} 25 | |> Supervisor.child_spec(shutdown: server_config.shutdown_timeout) 26 | 27 | # Then try to create it 28 | do_start( 29 | sup_pid, 30 | child_spec, 31 | raw_socket, 32 | server_config, 33 | acceptor_span, 34 | start_time, 35 | server_config.max_connections_retry_count 36 | ) 37 | end 38 | 39 | defp do_start( 40 | sup_pid, 41 | child_spec, 42 | raw_socket, 43 | server_config, 44 | acceptor_span, 45 | start_time, 46 | retries 47 | ) do 48 | case DynamicSupervisor.start_child(sup_pid, child_spec) do 49 | {:ok, pid} -> 50 | # Since this process owns the socket at this point, it needs to be the 51 | # one to make this call. connection_pid is sitting and waiting for the 52 | # word from us to start processing, in order to ensure that we've made 53 | # the following call. Note that we purposefully do not match on the 54 | # return from this function; if there's an error the connection process 55 | # will see it, but it's no longer our problem if that's the case 56 | _ = server_config.transport_module.controlling_process(raw_socket, pid) 57 | 58 | # Now that we have transferred ownership over to the new process, send a message to the 59 | # new process with all the info it needs to start working with the socket (note that the 60 | # new process will still need to handshake with the remote end) 61 | send(pid, {:thousand_island_ready, raw_socket, server_config, acceptor_span, start_time}) 62 | 63 | :ok 64 | 65 | {:error, :max_children} when retries > 0 -> 66 | # We're in a tricky spot here; we have a client connection in hand, but no room to put it 67 | # into the connection supervisor. We try to wait a maximum number of times to see if any 68 | # room opens up before we give up 69 | Process.sleep(server_config.max_connections_retry_wait) 70 | 71 | do_start( 72 | sup_pid, 73 | child_spec, 74 | raw_socket, 75 | server_config, 76 | acceptor_span, 77 | start_time, 78 | retries - 1 79 | ) 80 | 81 | {:error, :max_children} -> 82 | # We gave up trying to find room for this connection in our supervisor. 83 | # Close the raw socket here and let the acceptor process handle propagating the error 84 | server_config.transport_module.close(raw_socket) 85 | {:error, :too_many_connections} 86 | 87 | other -> 88 | other 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/thousand_island/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Handler do 2 | @moduledoc """ 3 | `ThousandIsland.Handler` defines the behaviour required of the application layer of a Thousand Island server. When starting a 4 | Thousand Island server, you must pass the name of a module implementing this behaviour as the `handler_module` parameter. 5 | Thousand Island will then use the specified module to handle each connection that is made to the server. 6 | 7 | The lifecycle of a Handler instance is as follows: 8 | 9 | 1. After a client connection to a Thousand Island server is made, Thousand Island will complete the initial setup of the 10 | connection (performing a TLS handshake, for example), and then call `c:handle_connection/2`. 11 | 12 | 2. A handler implementation may choose to process a client connection within the `c:handle_connection/2` callback by 13 | calling functions against the passed `ThousandIsland.Socket`. In many cases, this may be all that may be required of 14 | an implementation & the value `{:close, state}` can be returned which will cause Thousand Island to close the connection 15 | to the client. 16 | 17 | 3. In cases where the server wishes to keep the connection open and wait for subsequent requests from the client on the 18 | same socket, it may elect to return `{:continue, state}`. This will cause Thousand Island to wait for client data 19 | asynchronously; `c:handle_data/3` will be invoked when the client sends more data. 20 | 21 | 4. In the meantime, the process which is hosting connection is idle & able to receive messages sent from elsewhere in your 22 | application as needed. The implementation included in the `use ThousandIsland.Handler` macro uses a `GenServer` structure, 23 | so you may implement such behaviour via standard `GenServer` patterns. Note that in these cases that state is provided (and 24 | must be returned) in a `{socket, state}` format, where the second tuple is the same state value that is passed to the various `handle_*` callbacks 25 | defined on this behaviour. It also critical to maintain the socket's `read_timeout` value by 26 | ensuring the relevant timeout value is returned as your callback's final argument. Both of these 27 | concerns are illustrated in the following example: 28 | 29 | ```elixir 30 | defmodule ExampleHandler do 31 | use ThousandIsland.Handler 32 | 33 | # ...handle_data and other Handler callbacks 34 | 35 | @impl GenServer 36 | def handle_call(msg, from, {socket, state}) do 37 | # Do whatever you'd like with msg & from 38 | {:reply, :ok, {socket, state}, socket.read_timeout} 39 | end 40 | 41 | @impl GenServer 42 | def handle_cast(msg, {socket, state}) do 43 | # Do whatever you'd like with msg 44 | {:noreply, {socket, state}, socket.read_timeout} 45 | end 46 | 47 | @impl GenServer 48 | def handle_info(msg, {socket, state}) do 49 | # Do whatever you'd like with msg 50 | {:noreply, {socket, state}, socket.read_timeout} 51 | end 52 | end 53 | ``` 54 | 55 | It is fully supported to intermix synchronous `ThousandIsland.Socket.recv` calls with async return values from `c:handle_connection/2` 56 | and `c:handle_data/3` callbacks. 57 | 58 | # Example 59 | 60 | A simple example of a Hello World server is as follows: 61 | 62 | ```elixir 63 | defmodule HelloWorld do 64 | use ThousandIsland.Handler 65 | 66 | @impl ThousandIsland.Handler 67 | def handle_connection(socket, state) do 68 | ThousandIsland.Socket.send(socket, "Hello, World") 69 | {:close, state} 70 | end 71 | end 72 | ``` 73 | 74 | Another example of a server that echoes back all data sent to it is as follows: 75 | 76 | ```elixir 77 | defmodule Echo do 78 | use ThousandIsland.Handler 79 | 80 | @impl ThousandIsland.Handler 81 | def handle_data(data, socket, state) do 82 | ThousandIsland.Socket.send(socket, data) 83 | {:continue, state} 84 | end 85 | end 86 | ``` 87 | 88 | Note that in this example there is no `c:handle_connection/2` callback defined. The default implementation of this 89 | callback will simply return `{:continue, state}`, which is appropriate for cases where the client is the first 90 | party to communicate. 91 | 92 | Another example of a server which can send and receive messages asynchronously is as follows: 93 | 94 | ```elixir 95 | defmodule Messenger do 96 | use ThousandIsland.Handler 97 | 98 | @impl ThousandIsland.Handler 99 | def handle_data(msg, _socket, state) do 100 | IO.puts(msg) 101 | {:continue, state} 102 | end 103 | 104 | def handle_info({:send, msg}, {socket, state}) do 105 | ThousandIsland.Socket.send(socket, msg) 106 | {:noreply, {socket, state}, socket.read_timeout} 107 | end 108 | end 109 | ``` 110 | 111 | Note that in this example we make use of the fact that the handler process is really just a GenServer to send it messages 112 | which are able to make use of the underlying socket. This allows for bidirectional sending and receiving of messages in 113 | an asynchronous manner. 114 | 115 | You can pass options to the default handler underlying `GenServer` by passing a `genserver_options` key to `ThousandIsland.start_link/1` 116 | containing `t:GenServer.options/0` to be passed to the last argument of `GenServer.start_link/3`. 117 | 118 | Please note that you should not pass the `name` `t:GenServer.option/0`. If you need to register handler processes for 119 | later lookup and use, you should perform process registration in `handle_connection/2`, ensuring the handler process is 120 | registered only after the underlying connection is established and you have access to the connection socket and metadata 121 | via `ThousandIsland.Socket.peername/1`. 122 | 123 | For example, using a custom process registry via `Registry`: 124 | 125 | ```elixir 126 | 127 | defmodule Messenger do 128 | use ThousandIsland.Handler 129 | 130 | @impl ThousandIsland.Handler 131 | def handle_connection(socket, state) do 132 | {:ok, {ip, port}} = ThousandIsland.Socket.peername(socket) 133 | {:ok, _pid} = Registry.register(MessengerRegistry, {state[:my_key], address}, nil) 134 | {:continue, state} 135 | end 136 | 137 | @impl ThousandIsland.Handler 138 | def handle_data(data, socket, state) do 139 | ThousandIsland.Socket.send(socket, data) 140 | {:continue, state} 141 | end 142 | end 143 | ``` 144 | 145 | This example assumes you have started a `Registry` and registered it under the name `MessengerRegistry`. 146 | 147 | # When Handler Isn't Enough 148 | 149 | The `use ThousandIsland.Handler` implementation should be flexible enough to power just about any handler, however if 150 | this should not be the case for you, there is an escape hatch available. If you require more flexibility than the 151 | `ThousandIsland.Handler` behaviour provides, you are free to specify any module which implements `start_link/1` as the 152 | `handler_module` parameter. The process of getting from this new process to a ready-to-use socket is somewhat 153 | delicate, however. The steps required are as follows: 154 | 155 | 1. Thousand Island calls `start_link/1` on the configured `handler_module`, passing in a tuple 156 | consisting of the configured handler and genserver opts. This function is expected to return a 157 | conventional `GenServer.on_start()` style tuple. Note that this newly created process is not 158 | passed the connection socket immediately. 159 | 2. The raw `t:ThousandIsland.Transport.socket()` socket will be passed to the new process via a 160 | message of the form `{:thousand_island_ready, raw_socket, server_config, acceptor_span, 161 | start_time}`. 162 | 3. Your implenentation must turn this into a `to:ThousandIsland.Socket.t()` socket by using the 163 | `ThousandIsland.Socket.new/3` call. 164 | 4. Your implementation must then call `ThousandIsland.Socket.handshake/1` with the socket as the 165 | sole argument in order to finalize the setup of the socket. 166 | 5. The socket is now ready to use. 167 | 168 | In addition to this process, there are several other considerations to be aware of: 169 | 170 | * The underlying socket is closed automatically when the handler process ends. 171 | 172 | * Handler processes should have a restart strategy of `:temporary` to ensure that Thousand Island does not attempt to 173 | restart crashed handlers. 174 | 175 | * Handler processes should trap exit if possible so that existing connections can be given a chance to cleanly shut 176 | down when shutting down a Thousand Island server instance. 177 | 178 | * Some of the `:connection` family of telemetry span events are emitted by the 179 | `ThousandIsland.Handler` implementation. If you use your own implementation in its place it is 180 | likely that such spans will not behave as expected. 181 | """ 182 | 183 | @typedoc "The possible ways to indicate a timeout when returning values to Thousand Island" 184 | @type timeout_options :: timeout() | {:persistent, timeout()} 185 | 186 | @typedoc "The value returned by `c:handle_connection/2` and `c:handle_data/3`" 187 | @type handler_result :: 188 | {:continue, state :: term()} 189 | | {:continue, state :: term(), timeout_options() | {:continue, term()}} 190 | | {:switch_transport, {module(), upgrade_opts :: [term()]}, state :: term()} 191 | | {:switch_transport, {module(), upgrade_opts :: [term()]}, state :: term(), 192 | timeout_options() | {:continue, term()}} 193 | | {:close, state :: term()} 194 | | {:error, term(), state :: term()} 195 | 196 | @doc """ 197 | This callback is called shortly after a client connection has been made, immediately after the socket handshake process has 198 | completed. It is called with the server's configured `handler_options` value as initial state. Handlers may choose to 199 | interact synchronously with the socket in this callback via calls to various `ThousandIsland.Socket` functions. 200 | 201 | The value returned by this callback causes Thousand Island to proceed in one of several ways: 202 | 203 | * Returning `{:close, state}` will cause Thousand Island to close the socket & call the `c:handle_close/2` callback to 204 | allow final cleanup to be done. 205 | * Returning `{:continue, state}` will cause Thousand Island to switch the socket to an asynchronous mode. When the 206 | client subsequently sends data (or if there is already unread data waiting from the client), Thousand Island will call 207 | `c:handle_data/3` to allow this data to be processed. 208 | * Returning `{:continue, state, timeout}` is identical to the previous case with the 209 | addition of a timeout. If `timeout` milliseconds passes with no data being received or messages 210 | being sent to the process, the socket will be closed and `c:handle_timeout/2` will be called. 211 | Note that this timeout is not persistent; it applies only to the interval until the next message 212 | is received. In order to set a persistent timeout for all future messages (essentially 213 | overwriting the value of `read_timeout` that was set at server startup), a value of 214 | `{:persistent, timeout}` may be returned. 215 | * Returning `{:continue, state, {:continue, continue}}` is identical to the previous case with the 216 | addition of a `c:GenServer.handle_continue/2` callback being made immediately after, in line with 217 | similar behaviour on `GenServer` callbacks. 218 | * Returning `{:switch_transport, {module, opts}, state}` will cause Thousand Island to try switching the transport of the 219 | current socket. The `module` should be an Elixir module that implements the `ThousandIsland.Transport` behaviour. 220 | Thousand Island will call `c:ThousandIsland.Transport.upgrade/2` for the given module to upgrade the transport in-place. 221 | After a successful upgrade Thousand Island will switch the socket to an asynchronous mode, as if `{:continue, state}` 222 | was returned. As with `:continue` return values, there are also timeout-specifying variants of 223 | this return value. 224 | * Returning `{:error, reason, state}` will cause Thousand Island to close the socket & call the `c:handle_error/3` callback to 225 | allow final cleanup to be done. 226 | """ 227 | @callback handle_connection(socket :: ThousandIsland.Socket.t(), state :: term()) :: 228 | handler_result() 229 | 230 | @doc """ 231 | This callback is called whenever client data is received after `c:handle_connection/2` or `c:handle_data/3` have returned an 232 | `{:continue, state}` tuple. The data received is passed as the first argument, and handlers may choose to interact 233 | synchronously with the socket in this callback via calls to various `ThousandIsland.Socket` functions. 234 | 235 | The value returned by this callback causes Thousand Island to proceed in one of several ways: 236 | 237 | * Returning `{:close, state}` will cause Thousand Island to close the socket & call the `c:handle_close/2` callback to 238 | allow final cleanup to be done. 239 | * Returning `{:continue, state}` will cause Thousand Island to switch the socket to an asynchronous mode. When the 240 | client subsequently sends data (or if there is already unread data waiting from the client), Thousand Island will call 241 | `c:handle_data/3` to allow this data to be processed. 242 | * Returning `{:continue, state, timeout}` is identical to the previous case with the 243 | addition of a timeout. If `timeout` milliseconds passes with no data being received or messages 244 | being sent to the process, the socket will be closed and `c:handle_timeout/2` will be called. 245 | Note that this timeout is not persistent; it applies only to the interval until the next message 246 | is received. In order to set a persistent timeout for all future messages (essentially 247 | overwriting the value of `read_timeout` that was set at server startup), a value of 248 | `{:persistent, timeout}` may be returned. 249 | * Returning `{:continue, state, {:continue, continue}}` is identical to the previous case with the 250 | addition of a `c:GenServer.handle_continue/2` callback being made immediately after, in line with 251 | similar behaviour on `GenServer` callbacks. 252 | * Returning `{:error, reason, state}` will cause Thousand Island to close the socket & call the `c:handle_error/3` callback to 253 | allow final cleanup to be done. 254 | """ 255 | @callback handle_data(data :: binary(), socket :: ThousandIsland.Socket.t(), state :: term()) :: 256 | handler_result() 257 | 258 | @doc """ 259 | This callback is called when the underlying socket is closed by the remote end; it should perform any cleanup required 260 | as it is the last callback called before the process backing this connection is terminated. The underlying socket 261 | has already been closed by the time this callback is called. The return value is ignored. 262 | 263 | This callback is not called if the connection is explicitly closed via `ThousandIsland.Socket.close/1`, however it 264 | will be called in cases where `handle_connection/2` or `handle_data/3` return a `{:close, state}` tuple. 265 | """ 266 | @callback handle_close(socket :: ThousandIsland.Socket.t(), state :: term()) :: term() 267 | 268 | @doc """ 269 | This callback is called when the underlying socket encounters an error; it should perform any cleanup required 270 | as it is the last callback called before the process backing this connection is terminated. The underlying socket 271 | has already been closed by the time this callback is called. The return value is ignored. 272 | 273 | In addition to socket level errors, this callback is also called in cases where `handle_connection/2` or `handle_data/3` 274 | return a `{:error, reason, state}` tuple, or when connection handshaking (typically TLS 275 | negotiation) fails. 276 | """ 277 | @callback handle_error(reason :: any(), socket :: ThousandIsland.Socket.t(), state :: term()) :: 278 | term() 279 | 280 | @doc """ 281 | This callback is called when the server process itself is being shut down; it should perform any cleanup required 282 | as it is the last callback called before the process backing this connection is terminated. The underlying socket 283 | has NOT been closed by the time this callback is called. The return value is ignored. 284 | 285 | This callback is only called when the shutdown reason is `:normal`, and is subject to the same caveats described 286 | in `c:GenServer.terminate/2`. 287 | """ 288 | @callback handle_shutdown(socket :: ThousandIsland.Socket.t(), state :: term()) :: term() 289 | 290 | @doc """ 291 | This callback is called when a handler process has gone more than `timeout` ms without receiving 292 | either remote data or a local message. The value used for `timeout` defaults to the 293 | `read_timeout` value specified at server startup, and may be overridden on a one-shot or 294 | persistent basis based on values returned from `c:handle_connection/2` or `c:handle_data/3` 295 | calls. Note that it is NOT called on explicit `ThousandIsland.Socket.recv/3` calls as they have 296 | their own timeout semantics. The underlying socket has NOT been closed by the time this callback 297 | is called. The return value is ignored. 298 | """ 299 | @callback handle_timeout(socket :: ThousandIsland.Socket.t(), state :: term()) :: term() 300 | 301 | @optional_callbacks handle_connection: 2, 302 | handle_data: 3, 303 | handle_close: 2, 304 | handle_error: 3, 305 | handle_shutdown: 2, 306 | handle_timeout: 2 307 | 308 | @spec __using__(any) :: Macro.t() 309 | defmacro __using__(_opts) do 310 | quote location: :keep do 311 | @behaviour ThousandIsland.Handler 312 | 313 | use GenServer, restart: :temporary 314 | 315 | @spec start_link({handler_options :: term(), GenServer.options()}) :: GenServer.on_start() 316 | def start_link({handler_options, genserver_options}) do 317 | GenServer.start_link(__MODULE__, handler_options, genserver_options) 318 | end 319 | 320 | unquote(genserver_impl()) 321 | unquote(handler_impl()) 322 | end 323 | end 324 | 325 | @doc false 326 | defmacro add_handle_info_fallback(_module) do 327 | quote do 328 | def handle_info({msg, _raw_socket, _data}, _state) when msg in [:tcp, :ssl] do 329 | raise """ 330 | The callback's `state` doesn't match the expected `{socket, state}` form. 331 | Please ensure that you are returning a `{socket, state}` tuple from any 332 | `GenServer.handle_*` callbacks you have implemented 333 | """ 334 | end 335 | end 336 | end 337 | 338 | # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity 339 | def genserver_impl do 340 | quote do 341 | @impl true 342 | def init(handler_options) do 343 | Process.flag(:trap_exit, true) 344 | {:ok, {nil, handler_options}} 345 | end 346 | 347 | @impl true 348 | def handle_info( 349 | {:thousand_island_ready, raw_socket, server_config, acceptor_span, start_time}, 350 | {nil, state} 351 | ) do 352 | {ip, port} = 353 | case server_config.transport_module.peername(raw_socket) do 354 | {:ok, remote_info} -> 355 | remote_info 356 | 357 | {:error, reason} -> 358 | # the socket has been prematurely closed by the client, we can't do anything with it 359 | # so we just close the socket, stop the GenServer with the error reason and move on. 360 | _ = server_config.transport_module.close(raw_socket) 361 | throw({:stop, {:shutdown, {:premature_conn_closing, reason}}, {raw_socket, state}}) 362 | end 363 | 364 | span_meta = %{remote_address: ip, remote_port: port} 365 | 366 | connection_span = 367 | ThousandIsland.Telemetry.start_child_span( 368 | acceptor_span, 369 | :connection, 370 | %{monotonic_time: start_time}, 371 | span_meta 372 | ) 373 | 374 | socket = ThousandIsland.Socket.new(raw_socket, server_config, connection_span) 375 | ThousandIsland.Telemetry.span_event(connection_span, :ready) 376 | 377 | case ThousandIsland.Socket.handshake(socket) do 378 | {:ok, socket} -> {:noreply, {socket, state}, {:continue, :handle_connection}} 379 | {:error, reason} -> {:stop, {:shutdown, {:handshake, reason}}, {socket, state}} 380 | end 381 | catch 382 | {:stop, _, _} = stop -> stop 383 | end 384 | 385 | def handle_info( 386 | {msg, raw_socket, data}, 387 | {%ThousandIsland.Socket{socket: raw_socket} = socket, state} 388 | ) 389 | when msg in [:tcp, :ssl] do 390 | ThousandIsland.Telemetry.untimed_span_event(socket.span, :async_recv, %{data: data}) 391 | 392 | __MODULE__.handle_data(data, socket, state) 393 | |> ThousandIsland.Handler.handle_continuation(socket) 394 | end 395 | 396 | def handle_info( 397 | {msg, raw_socket}, 398 | {%ThousandIsland.Socket{socket: raw_socket} = socket, state} 399 | ) 400 | when msg in [:tcp_closed, :ssl_closed] do 401 | {:stop, {:shutdown, :peer_closed}, {socket, state}} 402 | end 403 | 404 | def handle_info( 405 | {msg, raw_socket, reason}, 406 | {%ThousandIsland.Socket{socket: raw_socket} = socket, state} 407 | ) 408 | when msg in [:tcp_error, :ssl_error] do 409 | {:stop, reason, {socket, state}} 410 | end 411 | 412 | def handle_info(:timeout, {%ThousandIsland.Socket{} = socket, state}) do 413 | {:stop, {:shutdown, :timeout}, {socket, state}} 414 | end 415 | 416 | @before_compile {ThousandIsland.Handler, :add_handle_info_fallback} 417 | 418 | # Use a continue pattern here so that we have committed the socket 419 | # to state in case the `c:handle_connection/2` callback raises an error. 420 | # This ensures that the `c:terminate/2` calls below are able to properly 421 | # close down the process 422 | @impl true 423 | def handle_continue(:handle_connection, {%ThousandIsland.Socket{} = socket, state}) do 424 | __MODULE__.handle_connection(socket, state) 425 | |> ThousandIsland.Handler.handle_continuation(socket) 426 | end 427 | 428 | # Called if the remote end closed the connection before we could initialize it 429 | @impl true 430 | def terminate({:shutdown, {:premature_conn_closing, _reason}}, {_raw_socket, _state}) do 431 | :ok 432 | end 433 | 434 | # Called by GenServer if we hit our read_timeout. Socket is still open 435 | def terminate({:shutdown, :timeout}, {%ThousandIsland.Socket{} = socket, state}) do 436 | _ = __MODULE__.handle_timeout(socket, state) 437 | ThousandIsland.Handler.do_socket_close(socket, :timeout) 438 | end 439 | 440 | # Called if we're being shutdown in an orderly manner. Socket is still open 441 | def terminate(:shutdown, {%ThousandIsland.Socket{} = socket, state}) do 442 | _ = __MODULE__.handle_shutdown(socket, state) 443 | ThousandIsland.Handler.do_socket_close(socket, :shutdown) 444 | end 445 | 446 | # Called if the socket encountered an error during handshaking 447 | def terminate({:shutdown, {:handshake, reason}}, {%ThousandIsland.Socket{} = socket, state}) do 448 | _ = __MODULE__.handle_error(reason, socket, state) 449 | ThousandIsland.Handler.do_socket_close(socket, reason) 450 | end 451 | 452 | # Called if the socket encountered an error and we are configured to shutdown silently. 453 | # Socket is closed 454 | def terminate( 455 | {:shutdown, {:silent_termination, reason}}, 456 | {%ThousandIsland.Socket{} = socket, state} 457 | ) do 458 | _ = __MODULE__.handle_error(reason, socket, state) 459 | ThousandIsland.Handler.do_socket_close(socket, reason) 460 | end 461 | 462 | # Called if the socket encountered an error during upgrading 463 | def terminate({:shutdown, {:upgrade, reason}}, {socket, state}) do 464 | _ = __MODULE__.handle_error(reason, socket, state) 465 | ThousandIsland.Handler.do_socket_close(socket, reason) 466 | end 467 | 468 | # Called if the remote end shut down the connection, or if the local end closed the 469 | # connection by returning a `{:close,...}` tuple (in which case the socket will be open) 470 | def terminate({:shutdown, reason}, {%ThousandIsland.Socket{} = socket, state}) do 471 | _ = __MODULE__.handle_close(socket, state) 472 | ThousandIsland.Handler.do_socket_close(socket, reason) 473 | end 474 | 475 | # Called if the socket encountered an error. Socket is closed 476 | def terminate(reason, {%ThousandIsland.Socket{} = socket, state}) do 477 | _ = __MODULE__.handle_error(reason, socket, state) 478 | ThousandIsland.Handler.do_socket_close(socket, reason) 479 | end 480 | 481 | # This clause could happen if we do not have a socket defined in state (either because the 482 | # process crashed before setting it up, or because the user sent an invalid state) 483 | def terminate(_reason, _state) do 484 | :ok 485 | end 486 | end 487 | end 488 | 489 | def handler_impl do 490 | quote do 491 | @impl true 492 | def handle_connection(_socket, state), do: {:continue, state} 493 | 494 | @impl true 495 | def handle_data(_data, _socket, state), do: {:continue, state} 496 | 497 | @impl true 498 | def handle_close(_socket, _state), do: :ok 499 | 500 | @impl true 501 | def handle_error(_error, _socket, _state), do: :ok 502 | 503 | @impl true 504 | def handle_shutdown(_socket, _state), do: :ok 505 | 506 | @impl true 507 | def handle_timeout(_socket, _state), do: :ok 508 | 509 | defoverridable ThousandIsland.Handler 510 | end 511 | end 512 | 513 | @spec do_socket_close( 514 | ThousandIsland.Socket.t(), 515 | reason :: :shutdown | :local_closed | term() 516 | ) :: :ok 517 | @doc false 518 | def do_socket_close(socket, reason) do 519 | measurements = 520 | case ThousandIsland.Socket.getstat(socket) do 521 | {:ok, stats} -> 522 | stats 523 | |> Keyword.take([:send_oct, :send_cnt, :recv_oct, :recv_cnt]) 524 | |> Enum.into(%{}) 525 | 526 | _ -> 527 | %{} 528 | end 529 | 530 | metadata = 531 | if reason in [:shutdown, :local_closed, :peer_closed], do: %{}, else: %{error: reason} 532 | 533 | _ = ThousandIsland.Socket.close(socket) 534 | ThousandIsland.Telemetry.stop_span(socket.span, measurements, metadata) 535 | end 536 | 537 | @doc false 538 | def handle_continuation(continuation, socket) do 539 | case continuation do 540 | {:continue, state} -> 541 | _ = ThousandIsland.Socket.setopts(socket, active: :once) 542 | {:noreply, {socket, state}, socket.read_timeout} 543 | 544 | {:continue, state, {:continue, continue}} -> 545 | _ = ThousandIsland.Socket.setopts(socket, active: :once) 546 | {:noreply, {socket, state}, {:continue, continue}} 547 | 548 | {:continue, state, {:persistent, timeout}} -> 549 | socket = %{socket | read_timeout: timeout} 550 | _ = ThousandIsland.Socket.setopts(socket, active: :once) 551 | {:noreply, {socket, state}, timeout} 552 | 553 | {:continue, state, timeout} -> 554 | _ = ThousandIsland.Socket.setopts(socket, active: :once) 555 | {:noreply, {socket, state}, timeout} 556 | 557 | {:switch_transport, {module, upgrade_opts}, state} -> 558 | handle_switch_continuation(socket, module, upgrade_opts, state, socket.read_timeout) 559 | 560 | {:switch_transport, {module, upgrade_opts}, state, {:continue, continue}} -> 561 | handle_switch_continuation(socket, module, upgrade_opts, state, {:continue, continue}) 562 | 563 | {:switch_transport, {module, upgrade_opts}, state, {:persistent, timeout}} -> 564 | socket = %{socket | read_timeout: timeout} 565 | handle_switch_continuation(socket, module, upgrade_opts, state, timeout) 566 | 567 | {:switch_transport, {module, upgrade_opts}, state, timeout} -> 568 | handle_switch_continuation(socket, module, upgrade_opts, state, timeout) 569 | 570 | {:close, state} -> 571 | {:stop, {:shutdown, :local_closed}, {socket, state}} 572 | 573 | {:error, :timeout, state} -> 574 | {:stop, {:shutdown, :timeout}, {socket, state}} 575 | 576 | {:error, reason, state} -> 577 | if socket.silent_terminate_on_error do 578 | {:stop, {:shutdown, {:silent_termination, reason}}, {socket, state}} 579 | else 580 | {:stop, reason, {socket, state}} 581 | end 582 | end 583 | end 584 | 585 | defp handle_switch_continuation(socket, module, upgrade_opts, state, timeout_or_continue) do 586 | case ThousandIsland.Socket.upgrade(socket, module, upgrade_opts) do 587 | {:ok, socket} -> 588 | _ = ThousandIsland.Socket.setopts(socket, active: :once) 589 | {:noreply, {socket, state}, timeout_or_continue} 590 | 591 | {:error, reason} -> 592 | {:stop, {:shutdown, {:upgrade, reason}}, {socket, state}} 593 | end 594 | end 595 | end 596 | -------------------------------------------------------------------------------- /lib/thousand_island/listener.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Listener do 2 | @moduledoc false 3 | 4 | use GenServer, restart: :transient 5 | 6 | @type state :: %{ 7 | listener_socket: ThousandIsland.Transport.listener_socket(), 8 | listener_span: ThousandIsland.Telemetry.t(), 9 | local_info: ThousandIsland.Transport.socket_info() 10 | } 11 | 12 | @spec start_link(ThousandIsland.ServerConfig.t()) :: GenServer.on_start() 13 | def start_link(config), do: GenServer.start_link(__MODULE__, config) 14 | 15 | @spec stop(GenServer.server()) :: :ok 16 | def stop(server), do: GenServer.stop(server) 17 | 18 | @spec listener_info(GenServer.server()) :: ThousandIsland.Transport.socket_info() 19 | def listener_info(server), do: GenServer.call(server, :listener_info) 20 | 21 | @spec acceptor_info(GenServer.server()) :: 22 | {ThousandIsland.Transport.listener_socket(), ThousandIsland.Telemetry.t()} 23 | def acceptor_info(server), do: GenServer.call(server, :acceptor_info) 24 | 25 | @impl GenServer 26 | @spec init(ThousandIsland.ServerConfig.t()) :: {:ok, state} | {:stop, reason :: term} 27 | def init(%ThousandIsland.ServerConfig{} = server_config) do 28 | with {:ok, listener_socket} <- 29 | server_config.transport_module.listen( 30 | server_config.port, 31 | server_config.transport_options 32 | ), 33 | {:ok, {ip, port}} <- 34 | server_config.transport_module.sockname(listener_socket) do 35 | span_metadata = %{ 36 | handler: server_config.handler_module, 37 | local_address: ip, 38 | local_port: port, 39 | transport_module: server_config.transport_module, 40 | transport_options: server_config.transport_options 41 | } 42 | 43 | listener_span = ThousandIsland.Telemetry.start_span(:listener, %{}, span_metadata) 44 | 45 | {:ok, 46 | %{listener_socket: listener_socket, local_info: {ip, port}, listener_span: listener_span}} 47 | else 48 | {:error, reason} -> 49 | {:stop, reason} 50 | end 51 | end 52 | 53 | @impl GenServer 54 | @spec handle_call(:listener_info | :acceptor_info, any, state) :: 55 | {:reply, 56 | ThousandIsland.Transport.socket_info() 57 | | {ThousandIsland.Transport.listener_socket(), ThousandIsland.Telemetry.t()}, state} 58 | def handle_call(:listener_info, _from, state), do: {:reply, state.local_info, state} 59 | 60 | def handle_call(:acceptor_info, _from, state), 61 | do: {:reply, {state.listener_socket, state.listener_span}, state} 62 | 63 | @impl GenServer 64 | @spec terminate(reason, state) :: :ok 65 | when reason: :normal | :shutdown | {:shutdown, term} | term 66 | def terminate(_reason, state), do: ThousandIsland.Telemetry.stop_span(state.listener_span) 67 | end 68 | -------------------------------------------------------------------------------- /lib/thousand_island/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Logger do 2 | @moduledoc """ 3 | Logging conveniences for Thousand Island servers 4 | 5 | Allows dynamically adding and altering the log level used to trace connections 6 | within a Thousand Island server via the use of telemetry hooks. Should you wish 7 | to do your own logging or tracking of these events, a complete list of the 8 | telemetry events emitted by Thousand Island is described in the module 9 | documentation for `ThousandIsland.Telemetry`. 10 | """ 11 | 12 | require Logger 13 | 14 | @typedoc "Supported log levels" 15 | @type log_level :: :error | :info | :debug | :trace 16 | 17 | @doc """ 18 | Start logging Thousand Island at the specified log level. Valid values for log 19 | level are `:error`, `:info`, `:debug`, and `:trace`. Enabling a given log 20 | level implicitly enables all higher log levels as well. 21 | """ 22 | @spec attach_logger(log_level()) :: :ok | {:error, :already_exists} 23 | def attach_logger(:error) do 24 | events = [ 25 | [:thousand_island, :acceptor, :spawn_error], 26 | [:thousand_island, :acceptor, :econnaborted] 27 | ] 28 | 29 | :telemetry.attach_many("#{__MODULE__}.error", events, &__MODULE__.log_error/4, nil) 30 | end 31 | 32 | def attach_logger(:info) do 33 | _ = attach_logger(:error) 34 | 35 | events = [ 36 | [:thousand_island, :listener, :start], 37 | [:thousand_island, :listener, :stop] 38 | ] 39 | 40 | :telemetry.attach_many("#{__MODULE__}.info", events, &__MODULE__.log_info/4, nil) 41 | end 42 | 43 | def attach_logger(:debug) do 44 | _ = attach_logger(:info) 45 | 46 | events = [ 47 | [:thousand_island, :acceptor, :start], 48 | [:thousand_island, :acceptor, :stop], 49 | [:thousand_island, :connection, :start], 50 | [:thousand_island, :connection, :stop] 51 | ] 52 | 53 | :telemetry.attach_many("#{__MODULE__}.debug", events, &__MODULE__.log_debug/4, nil) 54 | end 55 | 56 | def attach_logger(:trace) do 57 | _ = attach_logger(:debug) 58 | 59 | events = [ 60 | [:thousand_island, :connection, :ready], 61 | [:thousand_island, :connection, :async_recv], 62 | [:thousand_island, :connection, :recv], 63 | [:thousand_island, :connection, :recv_error], 64 | [:thousand_island, :connection, :send], 65 | [:thousand_island, :connection, :send_error], 66 | [:thousand_island, :connection, :sendfile], 67 | [:thousand_island, :connection, :sendfile_error], 68 | [:thousand_island, :connection, :socket_shutdown] 69 | ] 70 | 71 | :telemetry.attach_many("#{__MODULE__}.trace", events, &__MODULE__.log_trace/4, nil) 72 | end 73 | 74 | @doc """ 75 | Stop logging Thousand Island at the specified log level. Disabling a given log 76 | level implicitly disables all lower log levels as well. 77 | """ 78 | @spec detach_logger(log_level()) :: :ok | {:error, :not_found} 79 | def detach_logger(:error) do 80 | _ = detach_logger(:info) 81 | :telemetry.detach("#{__MODULE__}.error") 82 | end 83 | 84 | def detach_logger(:info) do 85 | _ = detach_logger(:debug) 86 | :telemetry.detach("#{__MODULE__}.info") 87 | end 88 | 89 | def detach_logger(:debug) do 90 | _ = detach_logger(:trace) 91 | :telemetry.detach("#{__MODULE__}.debug") 92 | end 93 | 94 | def detach_logger(:trace) do 95 | :telemetry.detach("#{__MODULE__}.trace") 96 | end 97 | 98 | @doc false 99 | @spec log_error( 100 | :telemetry.event_name(), 101 | :telemetry.event_measurements(), 102 | :telemetry.event_metadata(), 103 | :telemetry.handler_config() 104 | ) :: :ok 105 | def log_error(event, measurements, metadata, _config) do 106 | Logger.error( 107 | "#{inspect(event)} metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}" 108 | ) 109 | end 110 | 111 | @doc false 112 | @spec log_info( 113 | :telemetry.event_name(), 114 | :telemetry.event_measurements(), 115 | :telemetry.event_metadata(), 116 | :telemetry.handler_config() 117 | ) :: :ok 118 | def log_info(event, measurements, metadata, _config) do 119 | Logger.info( 120 | "#{inspect(event)} metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}" 121 | ) 122 | end 123 | 124 | @doc false 125 | @spec log_debug( 126 | :telemetry.event_name(), 127 | :telemetry.event_measurements(), 128 | :telemetry.event_metadata(), 129 | :telemetry.handler_config() 130 | ) :: :ok 131 | def log_debug(event, measurements, metadata, _config) do 132 | Logger.debug( 133 | "#{inspect(event)} metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}" 134 | ) 135 | end 136 | 137 | @doc false 138 | @spec log_trace( 139 | :telemetry.event_name(), 140 | :telemetry.event_measurements(), 141 | :telemetry.event_metadata(), 142 | :telemetry.handler_config() 143 | ) :: :ok 144 | def log_trace(event, measurements, metadata, _config) do 145 | Logger.debug( 146 | "#{inspect(event)} metadata: #{inspect(metadata)}, measurements: #{inspect(measurements)}" 147 | ) 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/thousand_island/server.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Server do 2 | @moduledoc false 3 | 4 | use Supervisor 5 | 6 | @spec start_link(ThousandIsland.ServerConfig.t()) :: Supervisor.on_start() 7 | def start_link(%ThousandIsland.ServerConfig{} = config) do 8 | Supervisor.start_link(__MODULE__, config, config.supervisor_options) 9 | end 10 | 11 | @spec listener_pid(Supervisor.supervisor()) :: pid() | nil 12 | def listener_pid(supervisor) do 13 | supervisor 14 | |> Supervisor.which_children() 15 | |> Enum.find_value(fn 16 | {:listener, listener_pid, _, _} when is_pid(listener_pid) -> 17 | listener_pid 18 | 19 | _ -> 20 | false 21 | end) 22 | end 23 | 24 | @spec acceptor_pool_supervisor_pid(Supervisor.supervisor()) :: pid() | nil 25 | def acceptor_pool_supervisor_pid(supervisor) do 26 | supervisor 27 | |> Supervisor.which_children() 28 | |> Enum.find_value(fn 29 | {:acceptor_pool_supervisor, acceptor_pool_sup_pid, _, _} 30 | when is_pid(acceptor_pool_sup_pid) -> 31 | acceptor_pool_sup_pid 32 | 33 | _ -> 34 | false 35 | end) 36 | end 37 | 38 | @spec suspend(Supervisor.supervisor()) :: :ok | :error 39 | def suspend(pid) do 40 | with pool_sup_pid when is_pid(pool_sup_pid) <- acceptor_pool_supervisor_pid(pid), 41 | :ok <- ThousandIsland.AcceptorPoolSupervisor.suspend(pool_sup_pid), 42 | :ok <- Supervisor.terminate_child(pid, :shutdown_listener), 43 | :ok <- Supervisor.terminate_child(pid, :listener) do 44 | :ok 45 | else 46 | _ -> :error 47 | end 48 | end 49 | 50 | @spec resume(Supervisor.supervisor()) :: :ok | :error 51 | def resume(pid) do 52 | with :ok <- wrap_restart_child(pid, :listener), 53 | :ok <- wrap_restart_child(pid, :shutdown_listener), 54 | pool_sup_pid when is_pid(pool_sup_pid) <- acceptor_pool_supervisor_pid(pid), 55 | :ok <- ThousandIsland.AcceptorPoolSupervisor.resume(pool_sup_pid) do 56 | :ok 57 | else 58 | _ -> :error 59 | end 60 | end 61 | 62 | defp wrap_restart_child(pid, id) do 63 | case Supervisor.restart_child(pid, id) do 64 | {:ok, _child} -> :ok 65 | {:error, reason} when reason in [:running, :restarting] -> :ok 66 | {:error, _reason} -> :error 67 | end 68 | end 69 | 70 | @impl Supervisor 71 | @spec init(ThousandIsland.ServerConfig.t()) :: 72 | {:ok, 73 | {Supervisor.sup_flags(), 74 | [Supervisor.child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}} 75 | def init(config) do 76 | children = [ 77 | {ThousandIsland.Listener, config} |> Supervisor.child_spec(id: :listener), 78 | {ThousandIsland.AcceptorPoolSupervisor, {self(), config}} 79 | |> Supervisor.child_spec(id: :acceptor_pool_supervisor), 80 | {ThousandIsland.ShutdownListener, self()} 81 | |> Supervisor.child_spec(id: :shutdown_listener) 82 | ] 83 | 84 | Supervisor.init(children, strategy: :rest_for_one) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/thousand_island/server_config.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.ServerConfig do 2 | @moduledoc """ 3 | Encapsulates the configuration of a ThousandIsland server instance 4 | 5 | This is used internally by `ThousandIsland.Handler` 6 | """ 7 | 8 | @typedoc "A set of configuration parameters for a ThousandIsland server instance" 9 | @type t :: %__MODULE__{ 10 | port: :inet.port_number(), 11 | transport_module: ThousandIsland.transport_module(), 12 | transport_options: ThousandIsland.transport_options(), 13 | handler_module: module(), 14 | handler_options: term(), 15 | genserver_options: GenServer.options(), 16 | supervisor_options: [Supervisor.option()], 17 | num_acceptors: pos_integer(), 18 | num_connections: non_neg_integer() | :infinity, 19 | max_connections_retry_count: non_neg_integer(), 20 | max_connections_retry_wait: timeout(), 21 | read_timeout: timeout(), 22 | shutdown_timeout: timeout(), 23 | silent_terminate_on_error: boolean() 24 | } 25 | 26 | defstruct port: 4000, 27 | transport_module: ThousandIsland.Transports.TCP, 28 | transport_options: [], 29 | handler_module: nil, 30 | handler_options: [], 31 | genserver_options: [], 32 | supervisor_options: [], 33 | num_acceptors: 100, 34 | num_connections: 16_384, 35 | max_connections_retry_count: 5, 36 | max_connections_retry_wait: 1000, 37 | read_timeout: 60_000, 38 | shutdown_timeout: 15_000, 39 | silent_terminate_on_error: false 40 | 41 | @spec new(ThousandIsland.options()) :: t() 42 | def new(opts \\ []) do 43 | if !:proplists.is_defined(:handler_module, opts), 44 | do: raise("No handler_module defined in server configuration") 45 | 46 | struct!(__MODULE__, opts) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/thousand_island/shutdown_listener.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.ShutdownListener do 2 | @moduledoc false 3 | 4 | # Used as part of the `ThousandIsland.Server` supervision tree to facilitate 5 | # stopping the server's listener process early in the shutdown process, in order 6 | # to allow existing connections to drain without accepting new ones 7 | 8 | use GenServer 9 | 10 | @type state :: %{ 11 | optional(:server_pid) => pid(), 12 | optional(:listener_pid) => pid() | nil 13 | } 14 | 15 | @doc false 16 | @spec start_link(pid()) :: :ignore | {:error, any} | {:ok, pid} 17 | def start_link(server_pid) do 18 | GenServer.start_link(__MODULE__, server_pid) 19 | end 20 | 21 | @doc false 22 | @impl GenServer 23 | @spec init(pid()) :: {:ok, state, {:continue, :setup_listener_pid}} 24 | def init(server_pid) do 25 | Process.flag(:trap_exit, true) 26 | {:ok, %{server_pid: server_pid}, {:continue, :setup_listener_pid}} 27 | end 28 | 29 | @doc false 30 | @impl GenServer 31 | @spec handle_continue(:setup_listener_pid, state) :: {:noreply, state} 32 | def handle_continue(:setup_listener_pid, %{server_pid: server_pid}) do 33 | listener_pid = ThousandIsland.Server.listener_pid(server_pid) 34 | {:noreply, %{listener_pid: listener_pid}} 35 | end 36 | 37 | @doc false 38 | @impl GenServer 39 | @spec terminate(reason, state) :: :ok 40 | when reason: :normal | :shutdown | {:shutdown, term} | term 41 | def terminate(_reason, %{listener_pid: listener_pid}) do 42 | ThousandIsland.Listener.stop(listener_pid) 43 | end 44 | 45 | def terminate(_reason, _state), do: :ok 46 | end 47 | -------------------------------------------------------------------------------- /lib/thousand_island/socket.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Socket do 2 | @moduledoc """ 3 | Encapsulates a client connection's underlying socket, providing a facility to 4 | read, write, and otherwise manipulate a connection from a client. 5 | """ 6 | 7 | @enforce_keys [:socket, :transport_module, :read_timeout, :silent_terminate_on_error, :span] 8 | defstruct @enforce_keys 9 | 10 | @typedoc "A reference to a socket along with metadata describing how to use it" 11 | @type t :: %__MODULE__{ 12 | socket: ThousandIsland.Transport.socket(), 13 | transport_module: module(), 14 | read_timeout: timeout(), 15 | silent_terminate_on_error: boolean(), 16 | span: ThousandIsland.Telemetry.t() 17 | } 18 | 19 | @doc """ 20 | Creates a new socket struct based on the passed parameters. 21 | 22 | This is normally called internally by `ThousandIsland.Handler` and does not need to be 23 | called by implementations which are based on `ThousandIsland.Handler` 24 | """ 25 | @spec new( 26 | ThousandIsland.Transport.socket(), 27 | ThousandIsland.ServerConfig.t(), 28 | ThousandIsland.Telemetry.t() 29 | ) :: t() 30 | def new(raw_socket, server_config, span) do 31 | %__MODULE__{ 32 | socket: raw_socket, 33 | transport_module: server_config.transport_module, 34 | read_timeout: server_config.read_timeout, 35 | silent_terminate_on_error: server_config.silent_terminate_on_error, 36 | span: span 37 | } 38 | end 39 | 40 | @doc """ 41 | Handshakes the underlying socket if it is required (as in the case of SSL sockets, for example). 42 | 43 | This is normally called internally by `ThousandIsland.Handler` and does not need to be 44 | called by implementations which are based on `ThousandIsland.Handler` 45 | """ 46 | @spec handshake(t()) :: ThousandIsland.Transport.on_handshake() 47 | def handshake(%__MODULE__{} = socket) do 48 | case socket.transport_module.handshake(socket.socket) do 49 | {:ok, inner_socket} -> 50 | {:ok, %{socket | socket: inner_socket}} 51 | 52 | {:error, reason} = err -> 53 | ThousandIsland.Telemetry.stop_span(socket.span, %{}, %{error: reason}) 54 | err 55 | end 56 | end 57 | 58 | @doc """ 59 | Upgrades the transport of the socket to use the specified transport module, performing any client 60 | handshaking that may be required. The passed options are blindly passed through to the new 61 | transport module. 62 | 63 | This is normally called internally by `ThousandIsland.Handler` and does not need to be 64 | called by implementations which are based on `ThousandIsland.Handler` 65 | """ 66 | @spec upgrade(t(), module(), term()) :: ThousandIsland.Transport.on_upgrade() 67 | def upgrade(%__MODULE__{} = socket, module, opts) when is_atom(module) do 68 | case module.upgrade(socket.socket, opts) do 69 | {:ok, updated_socket} -> 70 | {:ok, %__MODULE__{socket | socket: updated_socket, transport_module: module}} 71 | 72 | {:error, reason} = err -> 73 | ThousandIsland.Telemetry.stop_span(socket.span, %{}, %{error: reason}) 74 | err 75 | end 76 | end 77 | 78 | @doc """ 79 | Returns available bytes on the given socket. Up to `length` bytes will be 80 | returned (0 can be passed in to get the next 'available' bytes, typically the 81 | next packet). If insufficient bytes are available, the function can wait `timeout` 82 | milliseconds for data to arrive. 83 | """ 84 | @spec recv(t(), non_neg_integer(), timeout() | nil) :: ThousandIsland.Transport.on_recv() 85 | def recv(%__MODULE__{} = socket, length \\ 0, timeout \\ nil) do 86 | case socket.transport_module.recv(socket.socket, length, timeout || socket.read_timeout) do 87 | {:ok, data} = ok -> 88 | ThousandIsland.Telemetry.untimed_span_event(socket.span, :recv, %{data: data}) 89 | ok 90 | 91 | {:error, reason} = err -> 92 | ThousandIsland.Telemetry.span_event(socket.span, :recv_error, %{error: reason}) 93 | err 94 | end 95 | end 96 | 97 | @doc """ 98 | Sends the given data (specified as a binary or an IO list) on the given socket. 99 | """ 100 | @spec send(t(), iodata()) :: ThousandIsland.Transport.on_send() 101 | def send(%__MODULE__{} = socket, data) do 102 | case socket.transport_module.send(socket.socket, data) do 103 | :ok -> 104 | ThousandIsland.Telemetry.untimed_span_event(socket.span, :send, %{data: data}) 105 | :ok 106 | 107 | {:error, reason} = err -> 108 | ThousandIsland.Telemetry.span_event(socket.span, :send_error, %{data: data, error: reason}) 109 | 110 | err 111 | end 112 | end 113 | 114 | @doc """ 115 | Sends the contents of the given file based on the provided offset & length 116 | """ 117 | @spec sendfile(t(), String.t(), non_neg_integer(), non_neg_integer()) :: 118 | ThousandIsland.Transport.on_sendfile() 119 | def sendfile(%__MODULE__{} = socket, filename, offset, length) do 120 | case socket.transport_module.sendfile(socket.socket, filename, offset, length) do 121 | {:ok, bytes_written} = ok -> 122 | measurements = %{filename: filename, offset: offset, bytes_written: bytes_written} 123 | ThousandIsland.Telemetry.untimed_span_event(socket.span, :sendfile, measurements) 124 | ok 125 | 126 | {:error, reason} = err -> 127 | measurements = %{filename: filename, offset: offset, length: length, error: reason} 128 | ThousandIsland.Telemetry.span_event(socket.span, :sendfile_error, measurements) 129 | err 130 | end 131 | end 132 | 133 | @doc """ 134 | Shuts down the socket in the given direction. 135 | """ 136 | @spec shutdown(t(), ThousandIsland.Transport.way()) :: ThousandIsland.Transport.on_shutdown() 137 | def shutdown(%__MODULE__{} = socket, way) do 138 | ThousandIsland.Telemetry.span_event(socket.span, :socket_shutdown, %{way: way}) 139 | socket.transport_module.shutdown(socket.socket, way) 140 | end 141 | 142 | @doc """ 143 | Closes the given socket. Note that a socket is automatically closed when the handler 144 | process which owns it terminates 145 | """ 146 | @spec close(t()) :: ThousandIsland.Transport.on_close() 147 | def close(%__MODULE__{} = socket) do 148 | socket.transport_module.close(socket.socket) 149 | end 150 | 151 | @doc """ 152 | Gets the given flags on the socket 153 | 154 | Errors are usually from :inet.posix(), however, SSL module defines return type as any() 155 | """ 156 | @spec getopts(t(), ThousandIsland.Transport.socket_get_options()) :: 157 | ThousandIsland.Transport.on_getopts() 158 | def getopts(%__MODULE__{} = socket, options) do 159 | socket.transport_module.getopts(socket.socket, options) 160 | end 161 | 162 | @doc """ 163 | Sets the given flags on the socket 164 | 165 | Errors are usually from :inet.posix(), however, SSL module defines return type as any() 166 | """ 167 | @spec setopts(t(), ThousandIsland.Transport.socket_set_options()) :: 168 | ThousandIsland.Transport.on_setopts() 169 | def setopts(%__MODULE__{} = socket, options) do 170 | socket.transport_module.setopts(socket.socket, options) 171 | end 172 | 173 | @doc """ 174 | Returns information in the form of `t:ThousandIsland.Transport.socket_info()` about the local end of the socket. 175 | """ 176 | @spec sockname(t()) :: ThousandIsland.Transport.on_sockname() 177 | def sockname(%__MODULE__{} = socket) do 178 | socket.transport_module.sockname(socket.socket) 179 | end 180 | 181 | @doc """ 182 | Returns information in the form of `t:ThousandIsland.Transport.socket_info()` about the remote end of the socket. 183 | """ 184 | @spec peername(t()) :: ThousandIsland.Transport.on_peername() 185 | def peername(%__MODULE__{} = socket) do 186 | socket.transport_module.peername(socket.socket) 187 | end 188 | 189 | @doc """ 190 | Returns information in the form of `t:public_key.der_encoded()` about the peer certificate of the socket. 191 | """ 192 | @spec peercert(t()) :: ThousandIsland.Transport.on_peercert() 193 | def peercert(%__MODULE__{} = socket) do 194 | socket.transport_module.peercert(socket.socket) 195 | end 196 | 197 | @doc """ 198 | Returns whether or not this protocol is secure. 199 | """ 200 | @spec secure?(t()) :: boolean() 201 | def secure?(%__MODULE__{} = socket) do 202 | socket.transport_module.secure?() 203 | end 204 | 205 | @doc """ 206 | Returns statistics about the connection. 207 | """ 208 | @spec getstat(t()) :: ThousandIsland.Transport.socket_stats() 209 | def getstat(%__MODULE__{} = socket) do 210 | socket.transport_module.getstat(socket.socket) 211 | end 212 | 213 | @doc """ 214 | Returns information about the protocol negotiated during transport handshaking (if any). 215 | """ 216 | @spec negotiated_protocol(t()) :: ThousandIsland.Transport.on_negotiated_protocol() 217 | def negotiated_protocol(%__MODULE__{} = socket) do 218 | socket.transport_module.negotiated_protocol(socket.socket) 219 | end 220 | 221 | @doc """ 222 | Returns information about the SSL connection info, if transport is SSL. 223 | """ 224 | @spec connection_information(t()) :: ThousandIsland.Transport.on_connection_information() 225 | def connection_information(%__MODULE__{} = socket) do 226 | socket.transport_module.connection_information(socket.socket) 227 | end 228 | 229 | @doc """ 230 | Returns the telemetry span representing the lifetime of this socket 231 | """ 232 | @spec telemetry_span(t()) :: ThousandIsland.Telemetry.t() 233 | def telemetry_span(%__MODULE__{} = socket) do 234 | socket.span 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /lib/thousand_island/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Telemetry do 2 | @moduledoc """ 3 | The following telemetry spans are emitted by thousand_island 4 | 5 | ## `[:thousand_island, :listener, *]` 6 | 7 | Represents a Thousand Island server listening to a port 8 | 9 | This span is started by the following event: 10 | 11 | * `[:thousand_island, :listener, :start]` 12 | 13 | Represents the start of the span 14 | 15 | This event contains the following measurements: 16 | 17 | * `monotonic_time`: The time of this event, in `:native` units 18 | 19 | This event contains the following metadata: 20 | 21 | * `telemetry_span_context`: A unique identifier for this span 22 | * `local_address`: The IP address that the listener is bound to 23 | * `local_port`: The port that the listener is bound to 24 | * `transport_module`: The transport module in use 25 | * `transport_options`: Options passed to the transport module at startup 26 | 27 | 28 | This span is ended by the following event: 29 | 30 | * `[:thousand_island, :listener, :stop]` 31 | 32 | Represents the end of the span 33 | 34 | This event contains the following measurements: 35 | 36 | * `monotonic_time`: The time of this event, in `:native` units 37 | * `duration`: The span duration, in `:native` units 38 | 39 | This event contains the following metadata: 40 | 41 | * `telemetry_span_context`: A unique identifier for this span 42 | * `local_address`: The IP address that the listener is bound to 43 | * `local_port`: The port that the listener is bound to 44 | * `transport_module`: The transport module in use 45 | * `transport_options`: Options passed to the transport module at startup 46 | 47 | ## `[:thousand_island, :acceptor, *]` 48 | 49 | Represents a Thousand Island acceptor process listening for connections 50 | 51 | This span is started by the following event: 52 | 53 | * `[:thousand_island, :acceptor, :start]` 54 | 55 | Represents the start of the span 56 | 57 | This event contains the following measurements: 58 | 59 | * `monotonic_time`: The time of this event, in `:native` units 60 | 61 | This event contains the following metadata: 62 | 63 | * `telemetry_span_context`: A unique identifier for this span 64 | * `parent_telemetry_span_context`: The span context of the `:listener` which created this acceptor 65 | 66 | This span is ended by the following event: 67 | 68 | * `[:thousand_island, :acceptor, :stop]` 69 | 70 | Represents the end of the span 71 | 72 | This event contains the following measurements: 73 | 74 | * `monotonic_time`: The time of this event, in `:native` units 75 | * `duration`: The span duration, in `:native` units 76 | * `connections`: The number of client requests that the acceptor handled 77 | 78 | This event contains the following metadata: 79 | 80 | * `telemetry_span_context`: A unique identifier for this span 81 | * `parent_telemetry_span_context`: The span context of the `:listener` which created this acceptor 82 | * `error`: The error that caused the span to end, if it ended in error 83 | 84 | The following events may be emitted within this span: 85 | 86 | * `[:thousand_island, :acceptor, :spawn_error]` 87 | 88 | Thousand Island was unable to spawn a process to handle a connection. This occurs when too 89 | many connections are in progress; you may want to look at increasing the `num_connections` 90 | configuration parameter 91 | 92 | This event contains the following measurements: 93 | 94 | * `monotonic_time`: The time of this event, in `:native` units 95 | 96 | This event contains the following metadata: 97 | 98 | * `telemetry_span_context`: A unique identifier for this span 99 | 100 | * `[:thousand_island, :acceptor, :econnaborted]` 101 | 102 | Thousand Island was unable to spawn a process to handle a connection since the remote end 103 | closed before we could accept it. This usually occurs when it takes too long for your server 104 | to start processing a connection; you may want to look at tuning OS-level TCP parameters or 105 | adding more server capacity. 106 | 107 | This event contains the following measurements: 108 | 109 | * `monotonic_time`: The time of this event, in `:native` units 110 | 111 | This event contains the following metadata: 112 | 113 | * `telemetry_span_context`: A unique identifier for this span 114 | 115 | ## `[:thousand_island, :connection, *]` 116 | 117 | Represents Thousand Island handling a specific client request 118 | 119 | This span is started by the following event: 120 | 121 | * `[:thousand_island, :connection, :start]` 122 | 123 | Represents the start of the span 124 | 125 | This event contains the following measurements: 126 | 127 | * `monotonic_time`: The time of this event, in `:native` units 128 | 129 | This event contains the following metadata: 130 | 131 | * `telemetry_span_context`: A unique identifier for this span 132 | * `parent_telemetry_span_context`: The span context of the `:acceptor` span which accepted 133 | this connection 134 | * `remote_address`: The IP address of the connected client 135 | * `remote_port`: The port of the connected client 136 | 137 | This span is ended by the following event: 138 | 139 | * `[:thousand_island, :connection, :stop]` 140 | 141 | Represents the end of the span 142 | 143 | This event contains the following measurements: 144 | 145 | * `monotonic_time`: The time of this event, in `:native` units 146 | * `duration`: The span duration, in `:native` units 147 | * `send_oct`: The number of octets sent on the connection 148 | * `send_cnt`: The number of packets sent on the connection 149 | * `recv_oct`: The number of octets received on the connection 150 | * `recv_cnt`: The number of packets received on the connection 151 | 152 | This event contains the following metadata: 153 | 154 | * `telemetry_span_context`: A unique identifier for this span 155 | * `parent_telemetry_span_context`: The span context of the `:acceptor` span which accepted 156 | this connection 157 | * `remote_address`: The IP address of the connected client 158 | * `remote_port`: The port of the connected client 159 | * `error`: The error that caused the span to end, if it ended in error 160 | 161 | The following events may be emitted within this span: 162 | 163 | * `[:thousand_island, :connection, :ready]` 164 | 165 | Thousand Island has completed setting up the client connection 166 | 167 | This event contains the following measurements: 168 | 169 | * `monotonic_time`: The time of this event, in `:native` units 170 | 171 | This event contains the following metadata: 172 | 173 | * `telemetry_span_context`: A unique identifier for this span 174 | 175 | * `[:thousand_island, :connection, :async_recv]` 176 | 177 | Thousand Island has asynchronously received data from the client 178 | 179 | This event contains the following measurements: 180 | 181 | * `data`: The data received from the client 182 | 183 | This event contains the following metadata: 184 | 185 | * `telemetry_span_context`: A unique identifier for this span 186 | 187 | * `[:thousand_island, :connection, :recv]` 188 | 189 | Thousand Island has synchronously received data from the client 190 | 191 | This event contains the following measurements: 192 | 193 | * `data`: The data received from the client 194 | 195 | This event contains the following metadata: 196 | 197 | * `telemetry_span_context`: A unique identifier for this span 198 | 199 | * `[:thousand_island, :connection, :recv_error]` 200 | 201 | Thousand Island encountered an error reading data from the client 202 | 203 | This event contains the following measurements: 204 | 205 | * `error`: A description of the error 206 | 207 | This event contains the following metadata: 208 | 209 | * `telemetry_span_context`: A unique identifier for this span 210 | 211 | * `[:thousand_island, :connection, :send]` 212 | 213 | Thousand Island has sent data to the client 214 | 215 | This event contains the following measurements: 216 | 217 | * `data`: The data sent to the client 218 | 219 | This event contains the following metadata: 220 | 221 | * `telemetry_span_context`: A unique identifier for this span 222 | 223 | * `[:thousand_island, :connection, :send_error]` 224 | 225 | Thousand Island encountered an error sending data to the client 226 | 227 | This event contains the following measurements: 228 | 229 | * `data`: The data that was being sent to the client 230 | * `error`: A description of the error 231 | 232 | This event contains the following metadata: 233 | 234 | * `telemetry_span_context`: A unique identifier for this span 235 | 236 | * `[:thousand_island, :connection, :sendfile]` 237 | 238 | Thousand Island has sent a file to the client 239 | 240 | This event contains the following measurements: 241 | 242 | * `filename`: The filename containing data sent to the client 243 | * `offset`: The offset (in bytes) within the file sending started from 244 | * `bytes_written`: The number of bytes written 245 | 246 | This event contains the following metadata: 247 | 248 | * `telemetry_span_context`: A unique identifier for this span 249 | 250 | * `[:thousand_island, :connection, :sendfile_error]` 251 | 252 | Thousand Island encountered an error sending a file to the client 253 | 254 | This event contains the following measurements: 255 | 256 | * `filename`: The filename containing data that was being sent to the client 257 | * `offset`: The offset (in bytes) within the file where sending started from 258 | * `length`: The number of bytes that were attempted to send 259 | * `error`: A description of the error 260 | 261 | This event contains the following metadata: 262 | 263 | * `telemetry_span_context`: A unique identifier for this span 264 | 265 | * `[:thousand_island, :connection, :socket_shutdown]` 266 | 267 | Thousand Island has shutdown the client connection 268 | 269 | This event contains the following measurements: 270 | 271 | * `monotonic_time`: The time of this event, in `:native` units 272 | * `way`: The direction in which the socket was shut down 273 | 274 | This event contains the following metadata: 275 | 276 | * `telemetry_span_context`: A unique identifier for this span 277 | """ 278 | 279 | @enforce_keys [:span_name, :telemetry_span_context, :start_time, :start_metadata] 280 | defstruct @enforce_keys 281 | 282 | @type t :: %__MODULE__{ 283 | span_name: span_name(), 284 | telemetry_span_context: reference(), 285 | start_time: integer(), 286 | start_metadata: metadata() 287 | } 288 | 289 | @type span_name :: :listener | :acceptor | :connection 290 | @type metadata :: :telemetry.event_metadata() 291 | 292 | @typedoc false 293 | @type measurements :: :telemetry.event_measurements() 294 | 295 | @typedoc false 296 | @type event_name :: 297 | :ready 298 | | :spawn_error 299 | | :econnaborted 300 | | :recv_error 301 | | :send_error 302 | | :sendfile_error 303 | | :socket_shutdown 304 | 305 | @typedoc false 306 | @type untimed_event_name :: 307 | :async_recv 308 | | :stop 309 | | :recv 310 | | :send 311 | | :sendfile 312 | 313 | @app_name :thousand_island 314 | 315 | @doc false 316 | @spec start_span(span_name(), measurements(), metadata()) :: t() 317 | def start_span(span_name, measurements, metadata) do 318 | measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0) 319 | telemetry_span_context = make_ref() 320 | metadata = Map.put(metadata, :telemetry_span_context, telemetry_span_context) 321 | _ = event([span_name, :start], measurements, metadata) 322 | 323 | %__MODULE__{ 324 | span_name: span_name, 325 | telemetry_span_context: telemetry_span_context, 326 | start_time: measurements[:monotonic_time], 327 | start_metadata: metadata 328 | } 329 | end 330 | 331 | @doc false 332 | @spec start_child_span(t(), span_name(), measurements(), metadata()) :: t() 333 | def start_child_span(parent_span, span_name, measurements \\ %{}, metadata \\ %{}) do 334 | metadata = 335 | metadata 336 | |> Map.put(:parent_telemetry_span_context, parent_span.telemetry_span_context) 337 | |> Map.put(:handler, parent_span.start_metadata.handler) 338 | 339 | start_span(span_name, measurements, metadata) 340 | end 341 | 342 | @doc false 343 | @spec stop_span(t(), measurements(), metadata()) :: :ok 344 | def stop_span(span, measurements \\ %{}, metadata \\ %{}) do 345 | measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0) 346 | 347 | measurements = 348 | Map.put(measurements, :duration, measurements[:monotonic_time] - span.start_time) 349 | 350 | metadata = Map.merge(span.start_metadata, metadata) 351 | 352 | untimed_span_event(span, :stop, measurements, metadata) 353 | end 354 | 355 | @doc false 356 | @spec span_event(t(), event_name(), measurements(), metadata()) :: :ok 357 | def span_event(span, name, measurements \\ %{}, metadata \\ %{}) do 358 | measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0) 359 | untimed_span_event(span, name, measurements, metadata) 360 | end 361 | 362 | @doc false 363 | @spec untimed_span_event(t(), event_name() | untimed_event_name(), measurements(), metadata()) :: 364 | :ok 365 | def untimed_span_event(span, name, measurements \\ %{}, metadata \\ %{}) do 366 | metadata = 367 | metadata 368 | |> Map.put(:telemetry_span_context, span.telemetry_span_context) 369 | |> Map.put(:handler, span.start_metadata.handler) 370 | 371 | event([span.span_name, name], measurements, metadata) 372 | end 373 | 374 | @spec monotonic_time() :: integer 375 | defdelegate monotonic_time, to: System 376 | 377 | defp event(suffix, measurements, metadata) do 378 | :telemetry.execute([@app_name | suffix], measurements, metadata) 379 | end 380 | end 381 | -------------------------------------------------------------------------------- /lib/thousand_island/transport.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Transport do 2 | @moduledoc """ 3 | This module describes the behaviour required for Thousand Island to interact 4 | with low-level sockets. It is largely internal to Thousand Island, however users 5 | are free to implement their own versions of this behaviour backed by whatever 6 | underlying transport they choose. Such a module can be used in Thousand Island 7 | by passing its name as the `transport_module` option when starting up a server, 8 | as described in `ThousandIsland`. 9 | """ 10 | 11 | @typedoc "A listener socket used to wait for connections" 12 | @type listener_socket() :: :inet.socket() | :ssl.sslsocket() 13 | 14 | @typedoc "A listener socket options" 15 | @type listen_options() :: 16 | [:inet.inet_backend() | :gen_tcp.listen_option()] | [:ssl.tls_server_option()] 17 | 18 | @typedoc "A socket representing a client connection" 19 | @type socket() :: :inet.socket() | :ssl.sslsocket() 20 | 21 | @typedoc "Information about an endpoint, either remote ('peer') or local" 22 | @type socket_info() :: 23 | {:inet.ip_address(), :inet.port_number()} | :inet.returned_non_ip_address() 24 | 25 | @typedoc "A socket address" 26 | @type address :: 27 | :inet.ip_address() 28 | | :inet.local_address() 29 | | {:local, binary()} 30 | | :unspec 31 | | {:undefined, any()} 32 | @typedoc "Connection statistics for a given socket" 33 | @type socket_stats() :: {:ok, [{:inet.stat_option(), integer()}]} | {:error, :inet.posix()} 34 | 35 | @typedoc "Options which can be set on a socket via setopts/2 (or returned from getopts/1)" 36 | @type socket_get_options() :: [:inet.socket_getopt()] 37 | 38 | @typedoc "Options which can be set on a socket via setopts/2 (or returned from getopts/1)" 39 | @type socket_set_options() :: [:inet.socket_setopt()] 40 | 41 | @typedoc "The direction in which to shutdown a connection in advance of closing it" 42 | @type way() :: :read | :write | :read_write 43 | 44 | @typedoc "The return value from a listen/2 call" 45 | @type on_listen() :: 46 | {:ok, listener_socket()} | {:error, :system_limit} | {:error, :inet.posix()} 47 | 48 | @typedoc "The return value from an accept/1 call" 49 | @type on_accept() :: {:ok, socket()} | {:error, on_accept_tcp_error() | on_accept_ssl_error()} 50 | 51 | @type on_accept_tcp_error() :: :closed | :system_limit | :inet.posix() 52 | @type on_accept_ssl_error() :: :closed | :timeout | :ssl.error_alert() 53 | 54 | @typedoc "The return value from a controlling_process/2 call" 55 | @type on_controlling_process() :: :ok | {:error, :closed | :not_owner | :badarg | :inet.posix()} 56 | 57 | @typedoc "The return value from a handshake/1 call" 58 | @type on_handshake() :: {:ok, socket()} | {:error, on_handshake_ssl_error()} 59 | 60 | @type on_handshake_ssl_error() :: :closed | :timeout | :ssl.error_alert() 61 | 62 | @typedoc "The return value from a upgrade/2 call" 63 | @type on_upgrade() :: {:ok, socket()} | {:error, term()} 64 | 65 | @typedoc "The return value from a shutdown/2 call" 66 | @type on_shutdown() :: :ok | {:error, :inet.posix()} 67 | 68 | @typedoc "The return value from a close/1 call" 69 | @type on_close() :: :ok | {:error, any()} 70 | 71 | @typedoc "The return value from a recv/3 call" 72 | @type on_recv() :: {:ok, binary()} | {:error, :closed | :timeout | :inet.posix()} 73 | 74 | @typedoc "The return value from a send/2 call" 75 | @type on_send() :: :ok | {:error, :closed | {:timeout, rest_data :: binary()} | :inet.posix()} 76 | 77 | @typedoc "The return value from a sendfile/4 call" 78 | @type on_sendfile() :: 79 | {:ok, non_neg_integer()} 80 | | {:error, :inet.posix() | :closed | :badarg | :not_owner | :eof} 81 | 82 | @typedoc "The return value from a getopts/2 call" 83 | @type on_getopts() :: {:ok, [:inet.socket_optval()]} | {:error, :inet.posix()} 84 | 85 | @typedoc "The return value from a setopts/2 call" 86 | @type on_setopts() :: :ok | {:error, :inet.posix()} 87 | 88 | @typedoc "The return value from a sockname/1 call" 89 | @type on_sockname() :: {:ok, socket_info()} | {:error, :inet.posix()} 90 | 91 | @typedoc "The return value from a peername/1 call" 92 | @type on_peername() :: {:ok, socket_info()} | {:error, :inet.posix()} 93 | 94 | @typedoc "The return value from a peercert/1 call" 95 | @type on_peercert() :: {:ok, :public_key.der_encoded()} | {:error, reason :: any()} 96 | 97 | @typedoc "The return value from a connection_information/1 call" 98 | @type on_connection_information() :: {:ok, :ssl.connection_info()} | {:error, reason :: any()} 99 | 100 | @typedoc "The return value from a negotiated_protocol/1 call" 101 | @type on_negotiated_protocol() :: 102 | {:ok, binary()} | {:error, :protocol_not_negotiated | :closed} 103 | 104 | @doc """ 105 | Create and return a listener socket bound to the given port and configured per 106 | the provided options. 107 | """ 108 | @callback listen(:inet.port_number(), listen_options()) :: 109 | {:ok, listener_socket()} | {:error, any()} 110 | 111 | @doc """ 112 | Wait for a client connection on the given listener socket. This call blocks until 113 | such a connection arrives, or an error occurs (such as the listener socket being 114 | closed). 115 | """ 116 | @callback accept(listener_socket()) :: on_accept() 117 | 118 | @doc """ 119 | Performs an initial handshake on a new client connection (such as that done 120 | when negotiating an SSL connection). Transports which do not have such a 121 | handshake can simply pass the socket through unchanged. 122 | """ 123 | @callback handshake(socket()) :: on_handshake() 124 | 125 | @doc """ 126 | Performs an upgrade of an existing client connection (for example upgrading 127 | an already-established connection to SSL). Transports which do not support upgrading can return 128 | `{:error, :unsupported_upgrade}`. 129 | """ 130 | @callback upgrade(socket(), term()) :: on_upgrade() 131 | 132 | @doc """ 133 | Transfers ownership of the given socket to the given process. This will always 134 | be called by the process which currently owns the socket. 135 | """ 136 | @callback controlling_process(socket(), pid()) :: on_controlling_process() 137 | 138 | @doc """ 139 | Returns available bytes on the given socket. Up to `num_bytes` bytes will be 140 | returned (0 can be passed in to get the next 'available' bytes, typically the 141 | next packet). If insufficient bytes are available, the function can wait `timeout` 142 | milliseconds for data to arrive. 143 | """ 144 | @callback recv(socket(), num_bytes :: non_neg_integer(), timeout :: timeout()) :: on_recv() 145 | 146 | @doc """ 147 | Sends the given data (specified as a binary or an IO list) on the given socket. 148 | """ 149 | @callback send(socket(), data :: iodata()) :: on_send() 150 | 151 | @doc """ 152 | Sends the contents of the given file based on the provided offset & length 153 | """ 154 | @callback sendfile( 155 | socket(), 156 | filename :: String.t(), 157 | offset :: non_neg_integer(), 158 | length :: non_neg_integer() 159 | ) :: on_sendfile() 160 | 161 | @doc """ 162 | Gets the given options on the socket. 163 | """ 164 | @callback getopts(socket(), socket_get_options()) :: on_getopts() 165 | 166 | @doc """ 167 | Sets the given options on the socket. Should disallow setting of options which 168 | are not compatible with Thousand Island 169 | """ 170 | @callback setopts(socket(), socket_set_options()) :: on_setopts() 171 | 172 | @doc """ 173 | Shuts down the socket in the given direction. 174 | """ 175 | @callback shutdown(socket(), way()) :: on_shutdown() 176 | 177 | @doc """ 178 | Closes the given socket. 179 | """ 180 | @callback close(socket() | listener_socket()) :: on_close() 181 | 182 | @doc """ 183 | Returns information in the form of `t:socket_info()` about the local end of the socket. 184 | """ 185 | @callback sockname(socket() | listener_socket()) :: on_sockname() 186 | 187 | @doc """ 188 | Returns information in the form of `t:socket_info()` about the remote end of the socket. 189 | """ 190 | @callback peername(socket()) :: on_peername() 191 | 192 | @doc """ 193 | Returns the peer certificate for the given socket in the form of `t:public_key.der_encoded()`. 194 | If the socket is not secure, `{:error, :not_secure}` is returned. 195 | """ 196 | @callback peercert(socket()) :: on_peercert() 197 | 198 | @doc """ 199 | Returns whether or not this protocol is secure. 200 | """ 201 | @callback secure?() :: boolean() 202 | 203 | @doc """ 204 | Returns stats about the connection on the socket. 205 | """ 206 | @callback getstat(socket()) :: socket_stats() 207 | 208 | @doc """ 209 | Returns the protocol negotiated as part of handshaking. Most typically this is via TLS' 210 | ALPN or NPN extensions. If the underlying transport does not support protocol negotiation 211 | (or if one was not negotiated), `{:error, :protocol_not_negotiated}` is returned 212 | """ 213 | @callback negotiated_protocol(socket()) :: on_negotiated_protocol() 214 | 215 | @doc """ 216 | Returns the SSL connection_info for the given socket. If the socket is not secure, 217 | `{:error, :not_secure}` is returned. 218 | """ 219 | @callback connection_information(socket()) :: on_connection_information() 220 | end 221 | -------------------------------------------------------------------------------- /lib/thousand_island/transports/ssl.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Transports.SSL do 2 | @moduledoc """ 3 | Defines a `ThousandIsland.Transport` implementation based on TCP SSL sockets 4 | as provided by Erlang's `:ssl` module. For the most part, users of Thousand 5 | Island will only ever need to deal with this module via `transport_options` 6 | passed to `ThousandIsland` at startup time. A complete list of such options 7 | is defined via the `t::ssl.tls_server_option/0` type. This list can be somewhat 8 | difficult to decipher; by far the most common values to pass to this transport 9 | are the following: 10 | 11 | * `keyfile`: The path to a PEM encoded key to use for SSL 12 | * `certfile`: The path to a PEM encoded cert to use for SSL 13 | * `ip`: The IP to listen on. Can be specified as: 14 | * `{1, 2, 3, 4}` for IPv4 addresses 15 | * `{1, 2, 3, 4, 5, 6, 7, 8}` for IPv6 addresses 16 | * `:loopback` for local loopback 17 | * `:any` for all interfaces (ie: `0.0.0.0`) 18 | * `{:local, "/path/to/socket"}` for a Unix domain socket. If this option is used, the `port` 19 | option *must* be set to `0`. 20 | 21 | Unless overridden, this module uses the following default options: 22 | 23 | ```elixir 24 | backlog: 1024, 25 | nodelay: true, 26 | send_timeout: 30_000, 27 | send_timeout_close: true, 28 | reuseaddr: true 29 | ``` 30 | 31 | The following options are required for the proper operation of Thousand Island 32 | and cannot be overridden: 33 | 34 | ```elixir 35 | mode: :binary, 36 | active: false 37 | ``` 38 | """ 39 | 40 | @type options() :: [:ssl.tls_server_option()] 41 | @type listener_socket() :: :ssl.sslsocket() 42 | @type socket() :: :ssl.sslsocket() 43 | 44 | @behaviour ThousandIsland.Transport 45 | 46 | @hardcoded_options [mode: :binary, active: false] 47 | 48 | @impl ThousandIsland.Transport 49 | @spec listen(:inet.port_number(), [:ssl.tls_server_option()]) :: 50 | ThousandIsland.Transport.on_listen() 51 | def listen(port, user_options) do 52 | default_options = [ 53 | backlog: 1024, 54 | nodelay: true, 55 | send_timeout: 30_000, 56 | send_timeout_close: true, 57 | reuseaddr: true 58 | ] 59 | 60 | # We can't use Keyword functions here because :ssl accepts non-keyword style options 61 | resolved_options = 62 | Enum.uniq_by( 63 | @hardcoded_options ++ user_options ++ default_options, 64 | fn 65 | {key, _} when is_atom(key) -> key 66 | key when is_atom(key) -> key 67 | end 68 | ) 69 | 70 | if not Enum.any?( 71 | [:certs_keys, :keyfile, :key, :sni_hosts, :sni_fun], 72 | &:proplists.is_defined(&1, resolved_options) 73 | ) do 74 | raise "transport_options must include one of keyfile, key, sni_hosts or sni_fun" 75 | end 76 | 77 | if not Enum.any?( 78 | [:certs_keys, :certfile, :cert, :sni_hosts, :sni_fun], 79 | &:proplists.is_defined(&1, resolved_options) 80 | ) do 81 | raise "transport_options must include one of certfile, cert, sni_hosts or sni_fun" 82 | end 83 | 84 | :ssl.listen(port, resolved_options) 85 | end 86 | 87 | @impl ThousandIsland.Transport 88 | @spec accept(listener_socket()) :: ThousandIsland.Transport.on_accept() 89 | defdelegate accept(listener_socket), to: :ssl, as: :transport_accept 90 | 91 | @impl ThousandIsland.Transport 92 | @spec handshake(socket()) :: ThousandIsland.Transport.on_handshake() 93 | def handshake(socket) do 94 | case :ssl.handshake(socket) do 95 | {:ok, socket, _protocol_extensions} -> {:ok, socket} 96 | other -> other 97 | end 98 | end 99 | 100 | @impl ThousandIsland.Transport 101 | @spec upgrade(socket(), options()) :: ThousandIsland.Transport.on_upgrade() 102 | def upgrade(socket, opts) do 103 | case :ssl.handshake(socket, opts) do 104 | {:ok, socket, _protocol_extensions} -> {:ok, socket} 105 | other -> other 106 | end 107 | end 108 | 109 | @impl ThousandIsland.Transport 110 | @spec controlling_process(socket(), pid()) :: ThousandIsland.Transport.on_controlling_process() 111 | defdelegate controlling_process(socket, pid), to: :ssl 112 | 113 | @impl ThousandIsland.Transport 114 | @spec recv(socket(), non_neg_integer(), timeout()) :: ThousandIsland.Transport.on_recv() 115 | defdelegate recv(socket, length, timeout), to: :ssl 116 | 117 | @impl ThousandIsland.Transport 118 | @spec send(socket(), iodata()) :: ThousandIsland.Transport.on_send() 119 | defdelegate send(socket, data), to: :ssl 120 | 121 | @impl ThousandIsland.Transport 122 | @spec sendfile( 123 | socket(), 124 | filename :: String.t(), 125 | offset :: non_neg_integer(), 126 | length :: non_neg_integer() 127 | ) :: ThousandIsland.Transport.on_sendfile() 128 | def sendfile(socket, filename, offset, length) do 129 | # We can't use :file.sendfile here since it works on clear sockets, not ssl 130 | # sockets. Build our own (much slower and not optimized for large files) version. 131 | case :file.open(filename, [:raw]) do 132 | {:ok, fd} -> 133 | try do 134 | with {:ok, data} <- :file.pread(fd, offset, length), 135 | :ok <- :ssl.send(socket, data) do 136 | {:ok, length} 137 | else 138 | :eof -> {:error, :eof} 139 | {:error, reason} -> {:error, reason} 140 | end 141 | after 142 | :file.close(fd) 143 | end 144 | 145 | {:error, reason} -> 146 | {:error, reason} 147 | end 148 | end 149 | 150 | @impl ThousandIsland.Transport 151 | @spec getopts(socket(), ThousandIsland.Transport.socket_get_options()) :: 152 | ThousandIsland.Transport.on_getopts() 153 | defdelegate getopts(socket, options), to: :ssl 154 | 155 | @impl ThousandIsland.Transport 156 | @spec setopts(socket(), ThousandIsland.Transport.socket_set_options()) :: 157 | ThousandIsland.Transport.on_setopts() 158 | defdelegate setopts(socket, options), to: :ssl 159 | 160 | @impl ThousandIsland.Transport 161 | @spec shutdown(socket(), ThousandIsland.Transport.way()) :: 162 | ThousandIsland.Transport.on_shutdown() 163 | defdelegate shutdown(socket, way), to: :ssl 164 | 165 | @impl ThousandIsland.Transport 166 | @spec close(socket() | listener_socket()) :: ThousandIsland.Transport.on_close() 167 | defdelegate close(socket), to: :ssl 168 | 169 | # :ssl.sockname/1's typespec is incorrect 170 | @dialyzer {:no_match, sockname: 1} 171 | 172 | @impl ThousandIsland.Transport 173 | @spec sockname(socket() | listener_socket()) :: ThousandIsland.Transport.on_sockname() 174 | defdelegate sockname(socket), to: :ssl 175 | 176 | # :ssl.peername/1's typespec is incorrect 177 | @dialyzer {:no_match, peername: 1} 178 | 179 | @impl ThousandIsland.Transport 180 | @spec peername(socket()) :: ThousandIsland.Transport.on_peername() 181 | defdelegate peername(socket), to: :ssl 182 | 183 | @impl ThousandIsland.Transport 184 | @spec peercert(socket()) :: ThousandIsland.Transport.on_peercert() 185 | defdelegate peercert(socket), to: :ssl 186 | 187 | @impl ThousandIsland.Transport 188 | @spec secure?() :: true 189 | def secure?, do: true 190 | 191 | @impl ThousandIsland.Transport 192 | @spec getstat(socket()) :: ThousandIsland.Transport.socket_stats() 193 | defdelegate getstat(socket), to: :ssl 194 | 195 | @impl ThousandIsland.Transport 196 | @spec negotiated_protocol(socket()) :: ThousandIsland.Transport.on_negotiated_protocol() 197 | defdelegate negotiated_protocol(socket), to: :ssl 198 | 199 | @impl ThousandIsland.Transport 200 | @spec connection_information(socket()) :: ThousandIsland.Transport.on_connection_information() 201 | defdelegate connection_information(socket), to: :ssl 202 | end 203 | -------------------------------------------------------------------------------- /lib/thousand_island/transports/tcp.ex: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.Transports.TCP do 2 | @moduledoc """ 3 | Defines a `ThousandIsland.Transport` implementation based on clear TCP sockets 4 | as provided by Erlang's `:gen_tcp` module. For the most part, users of Thousand 5 | Island will only ever need to deal with this module via `transport_options` 6 | passed to `ThousandIsland` at startup time. A complete list of such options 7 | is defined via the `t::gen_tcp.listen_option/0` type. This list can be somewhat 8 | difficult to decipher; by far the most common value to pass to this transport 9 | is the following: 10 | 11 | * `ip`: The IP to listen on. Can be specified as: 12 | * `{1, 2, 3, 4}` for IPv4 addresses 13 | * `{1, 2, 3, 4, 5, 6, 7, 8}` for IPv6 addresses 14 | * `:loopback` for local loopback 15 | * `:any` for all interfaces (i.e.: `0.0.0.0`) 16 | * `{:local, "/path/to/socket"}` for a Unix domain socket. If this option is used, 17 | the `port` option *must* be set to `0` 18 | 19 | Unless overridden, this module uses the following default options: 20 | 21 | ```elixir 22 | backlog: 1024, 23 | nodelay: true, 24 | send_timeout: 30_000, 25 | send_timeout_close: true, 26 | reuseaddr: true 27 | ``` 28 | 29 | The following options are required for the proper operation of Thousand Island 30 | and cannot be overridden: 31 | 32 | ```elixir 33 | mode: :binary, 34 | active: false 35 | ``` 36 | """ 37 | 38 | @type options() :: [:gen_tcp.listen_option()] 39 | @type listener_socket() :: :inet.socket() 40 | @type socket() :: :inet.socket() 41 | 42 | @behaviour ThousandIsland.Transport 43 | 44 | @hardcoded_options [mode: :binary, active: false] 45 | 46 | @impl ThousandIsland.Transport 47 | @spec listen(:inet.port_number(), [:inet.inet_backend() | :gen_tcp.listen_option()]) :: 48 | ThousandIsland.Transport.on_listen() 49 | def listen(port, user_options) do 50 | default_options = [ 51 | backlog: 1024, 52 | nodelay: true, 53 | send_timeout: 30_000, 54 | send_timeout_close: true, 55 | reuseaddr: true 56 | ] 57 | 58 | # We can't use Keyword functions here because :gen_tcp accepts non-keyword style options 59 | resolved_options = 60 | Enum.uniq_by( 61 | @hardcoded_options ++ user_options ++ default_options, 62 | fn 63 | {key, _} when is_atom(key) -> key 64 | key when is_atom(key) -> key 65 | end 66 | ) 67 | 68 | # `inet_backend`, if present, needs to be the first option 69 | sorted_options = 70 | Enum.sort(resolved_options, fn 71 | _, {:inet_backend, _} -> false 72 | _, _ -> true 73 | end) 74 | 75 | :gen_tcp.listen(port, sorted_options) 76 | end 77 | 78 | @impl ThousandIsland.Transport 79 | @spec accept(listener_socket()) :: ThousandIsland.Transport.on_accept() 80 | defdelegate accept(listener_socket), to: :gen_tcp 81 | 82 | @impl ThousandIsland.Transport 83 | @spec handshake(socket()) :: ThousandIsland.Transport.on_handshake() 84 | def handshake(socket), do: {:ok, socket} 85 | 86 | @impl ThousandIsland.Transport 87 | @spec upgrade(socket(), options()) :: ThousandIsland.Transport.on_upgrade() 88 | def upgrade(_, _), do: {:error, :unsupported_upgrade} 89 | 90 | @impl ThousandIsland.Transport 91 | @spec controlling_process(socket(), pid()) :: ThousandIsland.Transport.on_controlling_process() 92 | defdelegate controlling_process(socket, pid), to: :gen_tcp 93 | 94 | @impl ThousandIsland.Transport 95 | @spec recv(socket(), non_neg_integer(), timeout()) :: ThousandIsland.Transport.on_recv() 96 | defdelegate recv(socket, length, timeout), to: :gen_tcp 97 | 98 | @impl ThousandIsland.Transport 99 | @spec send(socket(), iodata()) :: ThousandIsland.Transport.on_send() 100 | defdelegate send(socket, data), to: :gen_tcp 101 | 102 | @impl ThousandIsland.Transport 103 | @spec sendfile( 104 | socket(), 105 | filename :: String.t(), 106 | offset :: non_neg_integer(), 107 | length :: non_neg_integer() 108 | ) :: ThousandIsland.Transport.on_sendfile() 109 | def sendfile(socket, filename, offset, length) do 110 | case :file.open(filename, [:raw]) do 111 | {:ok, fd} -> 112 | try do 113 | :file.sendfile(fd, socket, offset, length, []) 114 | after 115 | :file.close(fd) 116 | end 117 | 118 | {:error, reason} -> 119 | {:error, reason} 120 | end 121 | end 122 | 123 | @impl ThousandIsland.Transport 124 | @spec getopts(socket(), ThousandIsland.Transport.socket_get_options()) :: 125 | ThousandIsland.Transport.on_getopts() 126 | defdelegate getopts(socket, options), to: :inet 127 | 128 | @impl ThousandIsland.Transport 129 | @spec setopts(socket(), ThousandIsland.Transport.socket_set_options()) :: 130 | ThousandIsland.Transport.on_setopts() 131 | defdelegate setopts(socket, options), to: :inet 132 | 133 | @impl ThousandIsland.Transport 134 | @spec shutdown(socket(), ThousandIsland.Transport.way()) :: 135 | ThousandIsland.Transport.on_shutdown() 136 | defdelegate shutdown(socket, way), to: :gen_tcp 137 | 138 | @impl ThousandIsland.Transport 139 | @spec close(socket() | listener_socket()) :: :ok 140 | defdelegate close(socket), to: :gen_tcp 141 | 142 | @impl ThousandIsland.Transport 143 | @spec sockname(socket() | listener_socket()) :: ThousandIsland.Transport.on_sockname() 144 | defdelegate sockname(socket), to: :inet 145 | 146 | @impl ThousandIsland.Transport 147 | @spec peername(socket()) :: ThousandIsland.Transport.on_peername() 148 | defdelegate peername(socket), to: :inet 149 | 150 | @impl ThousandIsland.Transport 151 | @spec peercert(socket()) :: ThousandIsland.Transport.on_peercert() 152 | def peercert(_socket), do: {:error, :not_secure} 153 | 154 | @impl ThousandIsland.Transport 155 | @spec secure?() :: false 156 | def secure?, do: false 157 | 158 | @impl ThousandIsland.Transport 159 | @spec getstat(socket()) :: ThousandIsland.Transport.socket_stats() 160 | defdelegate getstat(socket), to: :inet 161 | 162 | @impl ThousandIsland.Transport 163 | @spec negotiated_protocol(socket()) :: ThousandIsland.Transport.on_negotiated_protocol() 164 | def negotiated_protocol(_socket), do: {:error, :protocol_not_negotiated} 165 | 166 | @impl ThousandIsland.Transport 167 | @spec connection_information(socket()) :: ThousandIsland.Transport.on_connection_information() 168 | def connection_information(_socket), do: {:error, :not_secure} 169 | end 170 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :thousand_island, 7 | version: "1.3.14", 8 | elixir: "~> 1.13", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | dialyzer: dialyzer(), 13 | name: "Thousand Island", 14 | description: "A simple & modern pure Elixir socket server", 15 | source_url: "https://github.com/mtrudel/thousand_island", 16 | package: [ 17 | maintainers: ["Mat Trudel"], 18 | licenses: ["MIT"], 19 | links: %{"GitHub" => "https://github.com/mtrudel/thousand_island"}, 20 | files: ["lib", "mix.exs", "README*", "LICENSE*", "CHANGELOG*"] 21 | ], 22 | docs: docs() 23 | ] 24 | end 25 | 26 | def application do 27 | [extra_applications: [:logger, :ssl]] 28 | end 29 | 30 | defp elixirc_paths(:test), do: ["lib", "test/support"] 31 | defp elixirc_paths(_), do: ["lib"] 32 | 33 | defp deps() do 34 | [ 35 | {:telemetry, "~> 0.4 or ~> 1.0"}, 36 | {:machete, ">= 0.0.0", only: [:dev, :test]}, 37 | {:ex_doc, "~> 0.25", only: [:dev, :test], runtime: false}, 38 | {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, 39 | {:credo, "~> 1.5", only: [:dev, :test], runtime: false} 40 | ] 41 | end 42 | 43 | defp dialyzer do 44 | [ 45 | plt_core_path: "priv/plts", 46 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, 47 | plt_add_deps: :apps_direct, 48 | plt_add_apps: [:public_key], 49 | flags: [ 50 | "-Werror_handling", 51 | "-Wextra_return", 52 | "-Wmissing_return", 53 | "-Wunknown", 54 | "-Wunmatched_returns", 55 | "-Wunderspecs" 56 | ] 57 | ] 58 | end 59 | 60 | defp docs do 61 | [ 62 | main: "ThousandIsland", 63 | logo: "assets/ex_doc_logo.png", 64 | groups_for_modules: [ 65 | Transport: [ 66 | ThousandIsland.Transport, 67 | ThousandIsland.Transports.TCP, 68 | ThousandIsland.Transports.SSL 69 | ] 70 | ] 71 | ] 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 4 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 6 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 7 | "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 8 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 9 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 10 | "machete": {:hex, :machete, "0.3.11", "64769196d9cf7a6d6192739c11152bf1c0ba12ca04029180c77188d8e292f962", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d58062c7bcf344645aba574d59f6547793d8a1d3413389b1b5d6084d05cae77"}, 11 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 12 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 13 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 14 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 15 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 16 | } 17 | -------------------------------------------------------------------------------- /test/support/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICyjCCAbICCQCRKhJFmW6v0zANBgkqhkiG9w0BAQsFADAnMQswCQYDVQQGEwJV 3 | UzEYMBYGA1UEAwwPRXhhbXBsZS1Sb290LUNBMB4XDTIxMDYwNjAzMDcxMVoXDTI3 4 | MDExNDAzMDcxMVowJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9v 5 | dC1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOdFzns6VXOtAsVw 6 | CxAVWFbfEEDzzJemaCmcnW7xxcPu/NutEKbrZzDPUW8FcRTK90i81rffcT12hEFV 7 | k66zVWVOxYFAGxqYhvrOaJlehwlWdmGCl2740UdsvXsziNXuKFYmZi3/8+rV4UJY 8 | s/Ipv61a8x/2KagCYykr2G03iAiF/C2j//xpjrUfSuGMu/lapDXxQfDsAGtd7qSc 9 | HHEMzBvLq8cLuwVJFQEBqvEU3Nkn3gAeW7gC6myGRH3c68mbWmY/r5RcS7KdFjsD 10 | BVMGZXOtzLozXzQqI/G4DmTxKvYqFN/pjz9PZ8vQFkr9ddrC09kNmxCcCbC3WQfa 11 | 3FihVM0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA1oErpO9Co61Wb+6mqYyFMZpy 12 | Bedyf3SIQzQlDig+R/+avs7ZMmZtu0A7LjMPdgpfrJhlBK/1ThkNRYRsMbnyku+F 13 | qC1XEnrfRG9PXYcxigH6DLTaHSfYVE5QQFQAZequiQb6K0LVWUcuPVkzZNGMl3RR 14 | qb9/pM2hCwwNApnZYi4SzH6tfhF6OFi5dNexCEqqtKd00ZZh09ERhnqZpR62jQS/ 15 | SERs6ZS9K9D+A9f/4cRSJOFB9gSfil0sqxBC5GhO6Bse5HEprlPkHPeoHMD1Mj1K 16 | zH1twf4Amnz3p9OGRZZ6qspkXhT+8gTIfzlcX/wJpv7c53uI/OAIxRwogI0w7Q== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/support/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDijCCAnKgAwIBAgIJAI8sEm5tQlsrMA0GCSqGSIb3DQEBCwUAMCcxCzAJBgNV 3 | BAYTAlVTMRgwFgYDVQQDDA9FeGFtcGxlLVJvb3QtQ0EwHhcNMjEwNjA2MDMwODMw 4 | WhcNNDgxMDIyMDMwODMwWjBtMQswCQYDVQQGEwJVUzESMBAGA1UECAwJWW91clN0 5 | YXRlMREwDwYDVQQHDAhZb3VyQ2l0eTEdMBsGA1UECgwURXhhbXBsZS1DZXJ0aWZp 6 | Y2F0ZXMxGDAWBgNVBAMMD2xvY2FsaG9zdC5sb2NhbDCCASIwDQYJKoZIhvcNAQEB 7 | BQADggEPADCCAQoCggEBAPN+9Lo3Z1DGmMLzj/5nb+48Wx4Ra4xBooeMyO22IqEu 8 | 93y4clasT8YZYoUh283AUUGajZaggia19HKSKAzmu2SFi4fRoqYYtyztsvm2qih2 9 | ORPGOJo0UUE7q5alM4RtgnNoAuPnjZ2eYAMEflt80K0X8TYAAfZ3wvfHEL5y8NDd 10 | 8LrBKsSVmE14bq7OzHMNPGuHEsTk/ESjBtJehQkQ1eT02TCgPPudTIky4jkkFQ/F 11 | OiMurScH+GsauKRZqSbhzv8a30FBl/50cTbnQb9KSfg0DjKEbP51NIX+PuSt9KlL 12 | 49r7AsKpSNqmpuVGSmInRVx+XOo4Ytt2Deu6lQAUzU8CAwEAAaNzMHEwQQYDVR0j 13 | BDowOKErpCkwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1D 14 | QYIJAJEqEkWZbq/TMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuC 15 | CWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAuTlHn/ytxq0QVtvFlEUXfWFi 16 | z0JUxTty7cwXPVVqxIYh5fVip6GYjlJgaQiMPeMneWbne6DtXckLvsje4XrjGeZO 17 | BYrsA4i6Ik6idxw3/AVyncJsNeA8fkEzyxFRUoAOLRrS7pb1EkuakgGuVv3c/gTa 18 | E1bAHzqQyEWW3i2H5hKBSjy5KD61MMcmD006dmypxmwaLmted28cgvqVR9fdU/5p 19 | vl6rnqUxEmTnKzX9LX4NQQR3lodyhj7zMVcL8ozC39YQ15oOSneDtMOweWmMAgE6 20 | idRfFBX/fBaprJKRAR9TGCCXcOO/cA9QTkI31iCdWCuqeCpKHtFSbpI8cehMZQ== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/support/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDzfvS6N2dQxpjC 3 | 84/+Z2/uPFseEWuMQaKHjMjttiKhLvd8uHJWrE/GGWKFIdvNwFFBmo2WoIImtfRy 4 | kigM5rtkhYuH0aKmGLcs7bL5tqoodjkTxjiaNFFBO6uWpTOEbYJzaALj542dnmAD 5 | BH5bfNCtF/E2AAH2d8L3xxC+cvDQ3fC6wSrElZhNeG6uzsxzDTxrhxLE5PxEowbS 6 | XoUJENXk9NkwoDz7nUyJMuI5JBUPxTojLq0nB/hrGrikWakm4c7/Gt9BQZf+dHE2 7 | 50G/Skn4NA4yhGz+dTSF/j7krfSpS+Pa+wLCqUjapqblRkpiJ0VcflzqOGLbdg3r 8 | upUAFM1PAgMBAAECggEAL2mEC5JoKqFQ83zrh9TqRZA5CcTIlTnehNhT831ohswX 9 | YpCjqt7IdcFRnqy2GP0elVCbyz2buh/p5jkxVTnEOVGLlrmqGv9rA3ORSvBXd6N1 10 | f7U0JkqTm8kboyytuFZ+dSxGi8v1lkBVX6ELXZMTKvEjhalAuJYfP5HiX8MPwwti 11 | 540zHN4fQ9cgDkpjLyR+g4us1iBtYmbx/+BHeE6tsN5I0dc4Ee/LxXLkBRqNhB4K 12 | g6wWynqLtX12py/TFaBfs8DSMY1IfdCxJDxiahtGCXFokfsQbqBySpLrdx41ela4 13 | IG5qr0u9/XyX4Y6pm4chNxKMwqRRF9bllGXEI2HySQKBgQD/pY43H3gPM/8IMzcT 14 | f3YvBX1KliU26xdbjPGrp+lAsUcAEv0gNmYsr6lUFZNd3RmsqfYR7Rt2GE468i95 15 | gfgRmTXUrDbZKeu92qvZNfd9f3HJYsjcHhSNNtRSYNubjpwniU4zyl78Jjz4jv44 16 | xNjEtmQ//FNTIIaKAr5zQ8lTxQKBgQDz1RoFwkJs38+LfLotjmNcmV7vC4Q4H3ZG 17 | m6mRNTfAiwFy9kbF16d4+fZ/byoHxSAOEjH08sKjQ1koH0moFQaFXWDW7/gwIiq3 18 | ErKO8ZE53uazFGOhNmHMdkdaTwb/Hn5/DKJBBexY/2O8S+tIZchhB0hluiz3QKSo 19 | p/rbcDiqAwKBgQDAXDFrltk/D0/qOqdJm5IxBX9mPR4ZecHkmGRMVpczn3EeRCuF 20 | LompPDA8XdO6QCEOhADtMi2EqftLbWp9kmc3zsHrmf3XYCzLeZvvYCUuoFPdReB/ 21 | iH7MVyJiLhFwtlkXgsB+Rds8/gTIvsfZrXyyX8+FOfb0yLeTZ0co8iuuRQKBgCgG 22 | hT0IxGqm2qTlFpK/2uOqcYD//PZRg9LXXqBtgfdjWhuK/dcgLWeYcLQ+hUG9RCPL 23 | LNQuvXCbb5k8eZTTzrw5tdnSjoUoNqbStOjuEo7TXj9rS2d9S9SKXfAfJODgGpe0 24 | dTYDSObbFX4lYDwEKT50OZgpVZRI0j61RGKdK1ANAoGBAKoO8Sbm6O9KTfvgVuQf 25 | t/L1c47JI6GnX1i9JCgQxRapmEYEupt0hRJh36zSvnip+iy8J22useJFtCcxeZUj 26 | XOH1WOWwQn8qDSUPA3PVO3TZb+k4Z8VlIKzrEWHLl56zWPO5Su7AHXseJhY1Z4Bz 27 | WmUvA9kdCmStQ7RMH89NRnh8 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/support/sendfile: -------------------------------------------------------------------------------- 1 | ABCDEF -------------------------------------------------------------------------------- /test/support/telemetry_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule TelemetryHelpers do 2 | @moduledoc false 3 | 4 | @events [ 5 | [:thousand_island, :listener, :start], 6 | [:thousand_island, :listener, :stop], 7 | [:thousand_island, :acceptor, :start], 8 | [:thousand_island, :acceptor, :stop], 9 | [:thousand_island, :acceptor, :spawn_error], 10 | [:thousand_island, :acceptor, :econnaborted], 11 | [:thousand_island, :connection, :start], 12 | [:thousand_island, :connection, :stop], 13 | [:thousand_island, :connection, :ready], 14 | [:thousand_island, :connection, :async_recv], 15 | [:thousand_island, :connection, :recv], 16 | [:thousand_island, :connection, :recv_error], 17 | [:thousand_island, :connection, :send], 18 | [:thousand_island, :connection, :send_error], 19 | [:thousand_island, :connection, :sendfile], 20 | [:thousand_island, :connection, :sendfile_error], 21 | [:thousand_island, :connection, :socket_shutdown] 22 | ] 23 | 24 | def attach_all_events(handler) do 25 | ref = make_ref() 26 | _ = :telemetry.attach_many(ref, @events, &__MODULE__.handle_event/4, {self(), handler}) 27 | fn -> :telemetry.detach(ref) end 28 | end 29 | 30 | def handle_event(event, measurements, %{handler: handler} = metadata, {pid, handler}), 31 | do: send(pid, {:telemetry, event, measurements, metadata}) 32 | 33 | def handle_event(_event, _measurements, _metadata, {_pid, _handler}), do: :ok 34 | end 35 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/thousand_island/listener_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.ListenerTest do 2 | use ExUnit.Case, async: true 3 | use Machete 4 | 5 | alias ThousandIsland.{Listener, ServerConfig} 6 | 7 | # We don't actually implement handler, but we specify it so that our telemetry helpers will work 8 | @server_config %ServerConfig{port: 4004, handler_module: __MODULE__} 9 | 10 | defmodule TestTransport do 11 | # This module does not implement all of the callbacks 12 | # used by the ThousandIsland.Transport behaviour, 13 | # but contains only the functions required 14 | # for the Listener to start successfully. 15 | 16 | def listen(port, _options) do 17 | send(self(), {:test_transport, port}) 18 | 19 | :gen_tcp.listen(port, 20 | mode: :binary, 21 | active: false 22 | ) 23 | end 24 | 25 | defdelegate sockname(socket), to: :inet 26 | end 27 | 28 | describe "init/1" do 29 | test "returns a :stop tuple if port cannot be bound" do 30 | # Bind to the port specified in the server config 31 | # so that it cannot be subsequently bound. 32 | assert {:ok, socket} = :gen_tcp.listen(@server_config.port, []) 33 | 34 | assert Listener.init(@server_config) == {:stop, :eaddrinuse} 35 | 36 | # Close the socket to cleanup. 37 | :gen_tcp.close(socket) 38 | end 39 | 40 | test "returns an :ok tuple with map containing :listener_socket, :local_info and :listener_span" do 41 | assert {:ok, 42 | %{ 43 | listener_socket: socket, 44 | local_info: {{0, 0, 0, 0}, port}, 45 | listener_span: %ThousandIsland.Telemetry{} 46 | }} = Listener.init(@server_config) 47 | 48 | assert port == @server_config.port 49 | 50 | # Close the socket to cleanup. 51 | :gen_tcp.close(socket) 52 | end 53 | 54 | test "listens using transport module specified in config" do 55 | {:ok, %{listener_socket: socket}} = 56 | Listener.init(%ServerConfig{@server_config | transport_module: TestTransport}) 57 | 58 | # 1) Listener.init/1 calls the listen/2 function 59 | # in the :transport_module with the :port as an argument 60 | # (:transport_module and :port are server config attributes). 61 | # 62 | # 2) The listen/2 function in the TestTransport module 63 | # sends a {:test_transport, _port} tuple to self() when called. 64 | # 65 | # Given (1) and (2), when Listener.init/1 is called 66 | # we can expect to receive the said tuple. 67 | assert_receive {:test_transport, port} 68 | assert @server_config.port == port 69 | 70 | # Close the socket to cleanup. 71 | :gen_tcp.close(socket) 72 | end 73 | 74 | test "listens on port specified in config" do 75 | # Confirm the port is not bound by asserting 76 | # that the port can be listened on, then cleanup 77 | # by closing the socket. 78 | assert {:ok, socket} = :gen_tcp.listen(@server_config.port, []) 79 | :gen_tcp.close(socket) 80 | 81 | {:ok, %{listener_socket: socket}} = 82 | Listener.init(@server_config) 83 | 84 | # Confirm the port is bound by asserting 85 | # that the port cannot be listened on, 86 | # as the port is in use. 87 | assert :gen_tcp.listen(@server_config.port, []) == {:error, :eaddrinuse} 88 | 89 | # Close the socket to cleanup. 90 | :gen_tcp.close(socket) 91 | end 92 | 93 | test "emits expected telemetry event" do 94 | TelemetryHelpers.attach_all_events(__MODULE__) 95 | 96 | {:ok, %{listener_socket: socket}} = Listener.init(@server_config) 97 | 98 | assert_receive {:telemetry, [:thousand_island, :listener, :start], measurements, metadata}, 99 | 500 100 | 101 | assert measurements ~> %{monotonic_time: integer()} 102 | 103 | assert metadata 104 | ~> %{ 105 | handler: __MODULE__, 106 | telemetry_span_context: reference(), 107 | local_address: {0, 0, 0, 0}, 108 | local_port: @server_config.port, 109 | transport_module: ThousandIsland.Transports.TCP, 110 | transport_options: [] 111 | } 112 | 113 | # Close the socket to cleanup. 114 | :gen_tcp.close(socket) 115 | end 116 | end 117 | 118 | describe "handle_call/3" do 119 | test "a :listener_info call gives a reply with the :local_info" do 120 | state = %{local_info: {{0, 0, 0, 0}, 4000}} 121 | 122 | assert Listener.handle_call(:listener_info, nil, state) == {:reply, state.local_info, state} 123 | end 124 | 125 | test "an :acceptor_info_info call gives a reply with the :listener_socket and :listener_span" do 126 | {:ok, %{listener_span: span, listener_socket: socket}} = 127 | Listener.init(@server_config) 128 | 129 | state = %{ 130 | listener_socket: socket, 131 | listener_span: span 132 | } 133 | 134 | assert Listener.handle_call(:acceptor_info, nil, state) == 135 | {:reply, {state.listener_socket, state.listener_span}, state} 136 | 137 | # Close the socket to cleanup. 138 | :gen_tcp.close(socket) 139 | end 140 | end 141 | 142 | describe "terminate/2" do 143 | test "emits telemetry event with expected timings" do 144 | {:ok, %{listener_span: span, listener_socket: socket}} = Listener.init(@server_config) 145 | 146 | TelemetryHelpers.attach_all_events(__MODULE__) 147 | 148 | Listener.terminate(:normal, %{listener_span: span}) 149 | 150 | assert_receive {:telemetry, [:thousand_island, :listener, :stop], measurements, metadata}, 151 | 500 152 | 153 | assert measurements ~> %{monotonic_time: integer(), duration: integer()} 154 | 155 | assert metadata 156 | ~> %{ 157 | handler: __MODULE__, 158 | telemetry_span_context: reference(), 159 | local_address: {0, 0, 0, 0}, 160 | local_port: @server_config.port, 161 | transport_module: ThousandIsland.Transports.TCP, 162 | transport_options: [] 163 | } 164 | 165 | # Close the socket to cleanup. 166 | :gen_tcp.close(socket) 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /test/thousand_island/server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.ServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | use Machete 5 | 6 | defmodule Echo do 7 | use ThousandIsland.Handler 8 | 9 | @impl ThousandIsland.Handler 10 | def handle_connection(socket, state) do 11 | {:ok, data} = ThousandIsland.Socket.recv(socket, 0) 12 | ThousandIsland.Socket.send(socket, data) 13 | {:close, state} 14 | end 15 | end 16 | 17 | defmodule LongEcho do 18 | use ThousandIsland.Handler 19 | 20 | @impl ThousandIsland.Handler 21 | def handle_data(data, socket, state) do 22 | ThousandIsland.Socket.send(socket, data) 23 | {:continue, state} 24 | end 25 | end 26 | 27 | defmodule Goodbye do 28 | use ThousandIsland.Handler 29 | 30 | @impl ThousandIsland.Handler 31 | def handle_shutdown(socket, state) do 32 | ThousandIsland.Socket.send(socket, "GOODBYE") 33 | {:close, state} 34 | end 35 | end 36 | 37 | defmodule ReadOpt do 38 | use ThousandIsland.Handler 39 | 40 | @impl ThousandIsland.Handler 41 | def handle_data(data, socket, state) do 42 | opts = [String.to_atom(data)] 43 | ThousandIsland.Socket.send(socket, inspect(ThousandIsland.Socket.getopts(socket, opts))) 44 | {:close, state} 45 | end 46 | end 47 | 48 | defmodule Error do 49 | use ThousandIsland.Handler 50 | 51 | @impl ThousandIsland.Handler 52 | def handle_error(error, _socket, state) do 53 | # Send error to test process 54 | case :proplists.get_value(:test_pid, state) do 55 | pid when is_pid(pid) -> 56 | send(pid, error) 57 | :ok 58 | 59 | _ -> 60 | raise "missing :test_pid for Error handler" 61 | end 62 | end 63 | end 64 | 65 | defmodule Whoami do 66 | use ThousandIsland.Handler 67 | 68 | @impl ThousandIsland.Handler 69 | def handle_connection(socket, state) do 70 | ThousandIsland.Socket.send(socket, :erlang.pid_to_list(self())) 71 | {:continue, state} 72 | end 73 | end 74 | 75 | test "should handle multiple connections as expected" do 76 | {:ok, _, port} = start_handler(Echo) 77 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 78 | {:ok, other_client} = :gen_tcp.connect(:localhost, port, active: false) 79 | 80 | :ok = :gen_tcp.send(client, "HELLO") 81 | :ok = :gen_tcp.send(other_client, "BONJOUR") 82 | 83 | # Invert the order to ensure we handle concurrently 84 | assert :gen_tcp.recv(other_client, 0) == {:ok, ~c"BONJOUR"} 85 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"HELLO"} 86 | 87 | :gen_tcp.close(client) 88 | :gen_tcp.close(other_client) 89 | end 90 | 91 | describe "num_connections handling" do 92 | test "should properly handle too many connections by queueing" do 93 | {:ok, _, port} = start_handler(LongEcho, num_acceptors: 1, num_connections: 1) 94 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 95 | {:ok, other_client} = :gen_tcp.connect(:localhost, port, active: false) 96 | 97 | :ok = :gen_tcp.send(client, "HELLO") 98 | :ok = :gen_tcp.send(other_client, "BONJOUR") 99 | 100 | # Give things enough time to send if they were going to 101 | Process.sleep(100) 102 | 103 | # Ensure that we haven't received anything on the second connection yet 104 | assert :gen_tcp.recv(other_client, 0, 10) == {:error, :timeout} 105 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"HELLO"} 106 | 107 | # Close our first connection to make room for the second to be accepted 108 | :gen_tcp.close(client) 109 | 110 | # Give things enough time to send if they were going to 111 | Process.sleep(100) 112 | 113 | # Ensure that the second connection unblocked 114 | assert :gen_tcp.recv(other_client, 0) == {:ok, ~c"BONJOUR"} 115 | :gen_tcp.close(other_client) 116 | end 117 | 118 | test "should properly handle too many connections if none close in time" do 119 | {:ok, _, port} = 120 | start_handler(LongEcho, 121 | num_acceptors: 1, 122 | num_connections: 1, 123 | max_connections_retry_wait: 100 124 | ) 125 | 126 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 127 | {:ok, other_client} = :gen_tcp.connect(:localhost, port, active: false) 128 | 129 | :ok = :gen_tcp.send(client, "HELLO") 130 | :ok = :gen_tcp.send(other_client, "BONJOUR") 131 | 132 | # Give things enough time to send if they were going to 133 | Process.sleep(100) 134 | 135 | # Ensure that we haven't received anything on the second connection yet 136 | assert :gen_tcp.recv(other_client, 0, 10) == {:error, :timeout} 137 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"HELLO"} 138 | 139 | # Give things enough time for the second connection to time out 140 | Process.sleep(500) 141 | 142 | # Ensure that the first connection is still open and the second connection closed 143 | :ok = :gen_tcp.send(client, "HELLO") 144 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"HELLO"} 145 | assert :gen_tcp.recv(other_client, 0) == {:error, :closed} 146 | :gen_tcp.close(other_client) 147 | 148 | # Close the first connection and ensure new connections are now accepted 149 | :gen_tcp.close(client) 150 | 151 | # Give things enough time for the first connection to time out 152 | Process.sleep(500) 153 | 154 | {:ok, third_client} = :gen_tcp.connect(:localhost, port, active: false) 155 | :ok = :gen_tcp.send(third_client, "BUONGIORNO") 156 | 157 | # Give things enough time to send if they were going to 158 | Process.sleep(100) 159 | 160 | assert :gen_tcp.recv(third_client, 0) == {:ok, ~c"BUONGIORNO"} 161 | end 162 | 163 | test "should emit telemetry events as expected" do 164 | TelemetryHelpers.attach_all_events(LongEcho) 165 | 166 | {:ok, _, port} = 167 | start_handler(LongEcho, 168 | num_acceptors: 1, 169 | num_connections: 1, 170 | max_connections_retry_wait: 100 171 | ) 172 | 173 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 174 | {:ok, other_client} = :gen_tcp.connect(:localhost, port, active: false) 175 | 176 | :ok = :gen_tcp.send(client, "HELLO") 177 | :ok = :gen_tcp.send(other_client, "BONJOUR") 178 | 179 | assert_receive {:telemetry, [:thousand_island, :acceptor, :spawn_error], measurements, 180 | metadata}, 181 | 1000 182 | 183 | assert measurements ~> %{monotonic_time: integer()} 184 | assert metadata ~> %{handler: LongEcho, telemetry_span_context: reference()} 185 | end 186 | end 187 | 188 | test "should enumerate active connection processes" do 189 | {:ok, server_pid, port} = start_handler(Whoami) 190 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 191 | {:ok, other_client} = :gen_tcp.connect(:localhost, port, active: false) 192 | 193 | {:ok, pid_1} = :gen_tcp.recv(client, 0) 194 | {:ok, pid_2} = :gen_tcp.recv(other_client, 0) 195 | pid_1 = :erlang.list_to_pid(pid_1) 196 | pid_2 = :erlang.list_to_pid(pid_2) 197 | 198 | {:ok, pids} = ThousandIsland.connection_pids(server_pid) 199 | assert Enum.sort(pids) == Enum.sort([pid_1, pid_2]) 200 | 201 | :gen_tcp.close(client) 202 | Process.sleep(100) 203 | 204 | assert {:ok, [pid_2]} == ThousandIsland.connection_pids(server_pid) 205 | 206 | :gen_tcp.close(other_client) 207 | Process.sleep(100) 208 | 209 | assert {:ok, []} == ThousandIsland.connection_pids(server_pid) 210 | end 211 | 212 | describe "suspend / resume" do 213 | test "suspend should stop accepting connections but keep existing ones open" do 214 | {:ok, server_pid, port} = start_handler(LongEcho, port: 9999) 215 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 216 | 217 | # Make sure the socket has transitioned ownership to the connection process 218 | Process.sleep(100) 219 | 220 | :ok = ThousandIsland.suspend(server_pid) 221 | 222 | # New connections should fail 223 | assert :gen_tcp.connect(:localhost, port, [active: false], 100) == {:error, :econnrefused} 224 | 225 | # But existing ones should still be open 226 | :ok = :gen_tcp.send(client, "HELLO") 227 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"HELLO"} 228 | 229 | # Now we resume the server 230 | :ok = ThousandIsland.resume(server_pid) 231 | 232 | # New connections should succeed 233 | {:ok, new_client} = :gen_tcp.connect(:localhost, port, active: false) 234 | :ok = :gen_tcp.send(new_client, "HELLO") 235 | assert :gen_tcp.recv(new_client, 0) == {:ok, ~c"HELLO"} 236 | :gen_tcp.close(new_client) 237 | 238 | # And existing ones should still be open 239 | :ok = :gen_tcp.send(client, "HELLO") 240 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"HELLO"} 241 | :gen_tcp.close(client) 242 | end 243 | end 244 | 245 | describe "shutdown" do 246 | test "it should stop accepting connections but allow existing ones to complete" do 247 | {:ok, server_pid, port} = start_handler(Echo) 248 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 249 | 250 | # Make sure the socket has transitioned ownership to the connection process 251 | Process.sleep(100) 252 | task = Task.async(fn -> ThousandIsland.stop(server_pid) end) 253 | # Make sure that the stop has had a chance to shutdown the acceptors 254 | Process.sleep(100) 255 | 256 | assert :gen_tcp.connect(:localhost, port, [active: false], 100) == {:error, :econnrefused} 257 | 258 | :ok = :gen_tcp.send(client, "HELLO") 259 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"HELLO"} 260 | :gen_tcp.close(client) 261 | 262 | Task.await(task) 263 | 264 | refute Process.alive?(server_pid) 265 | end 266 | 267 | test "it should give connections a chance to say goodbye" do 268 | {:ok, server_pid, port} = start_handler(Goodbye) 269 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 270 | 271 | # Make sure the socket has transitioned ownership to the connection process 272 | Process.sleep(100) 273 | task = Task.async(fn -> ThousandIsland.stop(server_pid) end) 274 | # Make sure that the stop has had a chance to shutdown the acceptors 275 | Process.sleep(100) 276 | 277 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"GOODBYE"} 278 | :gen_tcp.close(client) 279 | 280 | Task.await(task) 281 | 282 | refute Process.alive?(server_pid) 283 | end 284 | 285 | test "it should still work after a suspend / resume cycle" do 286 | {:ok, server_pid, port} = start_handler(Goodbye) 287 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 288 | 289 | # Make sure the socket has transitioned ownership to the connection process 290 | Process.sleep(100) 291 | 292 | :ok = ThousandIsland.suspend(server_pid) 293 | :ok = ThousandIsland.resume(server_pid) 294 | 295 | task = Task.async(fn -> ThousandIsland.stop(server_pid) end) 296 | # Make sure that the stop has had a chance to shutdown the acceptors 297 | Process.sleep(100) 298 | 299 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"GOODBYE"} 300 | :gen_tcp.close(client) 301 | 302 | Task.await(task) 303 | 304 | refute Process.alive?(server_pid) 305 | end 306 | 307 | test "it should forcibly shutdown connections after shutdown_timeout" do 308 | {:ok, server_pid, port} = start_handler(Echo, shutdown_timeout: 500) 309 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 310 | 311 | # Make sure the socket has transitioned ownership to the connection process 312 | Process.sleep(100) 313 | task = Task.async(fn -> ThousandIsland.stop(server_pid) end) 314 | # Make sure that the stop is still waiting on the open client, and the client is still alive 315 | Process.sleep(100) 316 | assert Process.alive?(server_pid) 317 | :gen_tcp.send(client, "HELLO") 318 | assert :gen_tcp.recv(client, 0) == {:ok, ~c"HELLO"} 319 | 320 | # Make sure that the stop finished by shutdown_timeout 321 | Process.sleep(500) 322 | refute Process.alive?(server_pid) 323 | 324 | # Clean up by waiting on the shutdown task 325 | Task.await(task) 326 | end 327 | 328 | test "it should emit telemetry events as expected" do 329 | TelemetryHelpers.attach_all_events(Echo) 330 | 331 | {:ok, server_pid, _} = start_handler(Echo, num_acceptors: 1) 332 | 333 | assert_receive {:telemetry, [:thousand_island, :listener, :start], measurements, metadata}, 334 | 500 335 | 336 | assert measurements ~> %{monotonic_time: integer()} 337 | 338 | assert metadata 339 | ~> %{ 340 | handler: Echo, 341 | telemetry_span_context: reference(), 342 | local_address: {0, 0, 0, 0}, 343 | local_port: integer(), 344 | transport_module: ThousandIsland.Transports.TCP, 345 | transport_options: [] 346 | } 347 | 348 | assert_receive {:telemetry, [:thousand_island, :acceptor, :start], measurements, metadata}, 349 | 500 350 | 351 | assert measurements ~> %{monotonic_time: integer()} 352 | 353 | assert metadata 354 | ~> %{ 355 | handler: Echo, 356 | telemetry_span_context: reference(), 357 | parent_telemetry_span_context: reference() 358 | } 359 | 360 | ThousandIsland.stop(server_pid) 361 | 362 | assert_receive {:telemetry, [:thousand_island, :acceptor, :stop], measurements, metadata}, 363 | 500 364 | 365 | assert measurements ~> %{monotonic_time: integer(), duration: integer(), connections: 0} 366 | 367 | assert metadata 368 | ~> %{ 369 | handler: Echo, 370 | telemetry_span_context: reference(), 371 | parent_telemetry_span_context: reference() 372 | } 373 | 374 | assert_receive {:telemetry, [:thousand_island, :listener, :stop], measurements, metadata}, 375 | 500 376 | 377 | assert measurements ~> %{monotonic_time: integer(), duration: integer()} 378 | 379 | assert metadata 380 | ~> %{ 381 | handler: Echo, 382 | telemetry_span_context: reference(), 383 | local_address: {0, 0, 0, 0}, 384 | local_port: integer(), 385 | transport_module: ThousandIsland.Transports.TCP, 386 | transport_options: [] 387 | } 388 | end 389 | end 390 | 391 | describe "configuration" do 392 | test "tcp should allow default options to be overridden" do 393 | {:ok, _, port} = start_handler(ReadOpt, transport_options: [send_timeout: 1230]) 394 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 395 | :gen_tcp.send(client, "send_timeout") 396 | {:ok, ~c"{:ok, [send_timeout: 1230]}"} = :gen_tcp.recv(client, 0, 100) 397 | end 398 | 399 | test "tcp should not allow hardcoded options to be overridden" do 400 | {:ok, _, port} = start_handler(ReadOpt, transport_options: [mode: :list]) 401 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 402 | :gen_tcp.send(client, "mode") 403 | {:ok, ~c"{:ok, [mode: :binary]}"} = :gen_tcp.recv(client, 0, 100) 404 | end 405 | 406 | test "tcp should allow Erlang style bare options" do 407 | {:ok, _, port} = start_handler(Echo, transport_options: [:inet6]) 408 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 409 | :gen_tcp.send(client, "HI") 410 | {:ok, ~c"HI"} = :gen_tcp.recv(client, 0, 100) 411 | end 412 | 413 | test "tcp should allow inet_backend option" do 414 | {:ok, _, port} = start_handler(Echo, transport_options: [inet_backend: :socket]) 415 | {:ok, client} = :gen_tcp.connect(:localhost, port, active: false) 416 | :gen_tcp.send(client, "HI") 417 | {:ok, ~c"HI"} = :gen_tcp.recv(client, 0, 100) 418 | end 419 | 420 | test "ssl should allow default options to be overridden" do 421 | {:ok, _, port} = 422 | start_handler(ReadOpt, 423 | transport_module: ThousandIsland.Transports.SSL, 424 | transport_options: [ 425 | send_timeout: 1230, 426 | certfile: Path.join(__DIR__, "../support/cert.pem"), 427 | keyfile: Path.join(__DIR__, "../support/key.pem") 428 | ] 429 | ) 430 | 431 | {:ok, client} = 432 | :ssl.connect(:localhost, port, 433 | active: false, 434 | verify: :verify_none, 435 | cacertfile: Path.join(__DIR__, "../support/ca.pem") 436 | ) 437 | 438 | :ssl.send(client, "send_timeout") 439 | {:ok, ~c"{:ok, [send_timeout: 1230]}"} = :ssl.recv(client, 0, 100) 440 | end 441 | 442 | test "ssl should not allow hardcoded options to be overridden" do 443 | {:ok, _, port} = 444 | start_handler(ReadOpt, 445 | transport_module: ThousandIsland.Transports.SSL, 446 | transport_options: [ 447 | mode: :list, 448 | certfile: Path.join(__DIR__, "../support/cert.pem"), 449 | keyfile: Path.join(__DIR__, "../support/key.pem") 450 | ] 451 | ) 452 | 453 | {:ok, client} = 454 | :ssl.connect(:localhost, port, 455 | active: false, 456 | verify: :verify_none, 457 | cacertfile: Path.join(__DIR__, "../support/ca.pem") 458 | ) 459 | 460 | :ssl.send(client, "mode") 461 | {:ok, ~c"{:ok, [mode: :binary]}"} = :ssl.recv(client, 0, 100) 462 | end 463 | 464 | test "ssl should allow Erlang style bare options" do 465 | {:ok, _, port} = 466 | start_handler(Echo, 467 | transport_module: ThousandIsland.Transports.SSL, 468 | transport_options: 469 | [:inet6] ++ 470 | [ 471 | certfile: Path.join(__DIR__, "../support/cert.pem"), 472 | keyfile: Path.join(__DIR__, "../support/key.pem") 473 | ] 474 | ) 475 | 476 | {:ok, client} = 477 | :ssl.connect(:localhost, port, 478 | active: false, 479 | verify: :verify_none, 480 | cacertfile: Path.join(__DIR__, "../support/ca.pem") 481 | ) 482 | 483 | :ssl.send(client, "HI") 484 | {:ok, ~c"HI"} = :ssl.recv(client, 0, 100) 485 | end 486 | end 487 | 488 | describe "invalid configuration" do 489 | @tag capture_log: true 490 | test "it should error if a certificate is not found" do 491 | {:ok, server_pid, port} = 492 | start_handler(Error, 493 | handler_options: [test_pid: self()], 494 | transport_module: ThousandIsland.Transports.SSL, 495 | transport_options: [ 496 | certfile: Path.join(__DIR__, "./not/a/cert.pem"), 497 | keyfile: Path.join(__DIR__, "./not/a/key.pem"), 498 | alpn_preferred_protocols: ["foo"] 499 | ] 500 | ) 501 | 502 | {:error, _} = 503 | :ssl.connect(~c"localhost", port, 504 | active: false, 505 | verify: :verify_peer, 506 | cacertfile: Path.join(__DIR__, "../support/ca.pem") 507 | ) 508 | 509 | Process.sleep(500) 510 | 511 | ThousandIsland.stop(server_pid) 512 | 513 | assert_received {:options, {:certfile, _, _}} 514 | end 515 | 516 | @tag capture_log: true 517 | test "handshake should fail if the client offers only unsupported ciphers" do 518 | server_args = [ 519 | port: 0, 520 | handler_module: Error, 521 | handler_options: [test_pid: self()], 522 | transport_module: ThousandIsland.Transports.SSL, 523 | transport_options: [ 524 | certfile: Path.join(__DIR__, "../support/cert.pem"), 525 | keyfile: Path.join(__DIR__, "../support/key.pem"), 526 | alpn_preferred_protocols: ["foo"] 527 | ] 528 | ] 529 | 530 | {:ok, server_pid} = start_supervised({ThousandIsland, server_args}) 531 | {:ok, {_ip, port}} = ThousandIsland.listener_info(server_pid) 532 | 533 | {:error, _} = 534 | :ssl.connect(~c"localhost", port, 535 | active: false, 536 | verify: :verify_peer, 537 | cacertfile: Path.join(__DIR__, "../support/ca.pem"), 538 | ciphers: [ 539 | %{cipher: :rc4_128, key_exchange: :rsa, mac: :md5, prf: :default_prf} 540 | ] 541 | ) 542 | 543 | Process.sleep(500) 544 | 545 | ThousandIsland.stop(server_pid) 546 | 547 | assert_received {:tls_alert, {:insufficient_security, _}} 548 | end 549 | end 550 | 551 | defp start_handler(handler, opts \\ []) do 552 | resolved_args = opts |> Keyword.put_new(:port, 0) |> Keyword.put(:handler_module, handler) 553 | {:ok, server_pid} = start_supervised({ThousandIsland, resolved_args}) 554 | {:ok, {_ip, port}} = ThousandIsland.listener_info(server_pid) 555 | {:ok, server_pid, port} 556 | end 557 | end 558 | -------------------------------------------------------------------------------- /test/thousand_island/socket_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ThousandIsland.SocketTest do 2 | use ExUnit.Case, async: true 3 | 4 | use Machete 5 | 6 | def gen_tcp_setup(_context) do 7 | {:ok, %{client_mod: :gen_tcp, client_opts: [active: false], server_opts: []}} 8 | end 9 | 10 | def ssl_setup(_context) do 11 | {:ok, 12 | %{ 13 | client_mod: :ssl, 14 | client_opts: [ 15 | active: false, 16 | verify: :verify_peer, 17 | cacertfile: Path.join(__DIR__, "../support/ca.pem") 18 | ], 19 | server_opts: [ 20 | transport_module: ThousandIsland.Transports.SSL, 21 | transport_options: [ 22 | certfile: Path.join(__DIR__, "../support/cert.pem"), 23 | keyfile: Path.join(__DIR__, "../support/key.pem"), 24 | alpn_preferred_protocols: ["foo"] 25 | ] 26 | ] 27 | }} 28 | end 29 | 30 | defmodule Echo do 31 | use ThousandIsland.Handler 32 | 33 | @impl ThousandIsland.Handler 34 | def handle_connection(socket, state) do 35 | {:ok, data} = ThousandIsland.Socket.recv(socket, 0) 36 | ThousandIsland.Socket.send(socket, data) 37 | {:close, state} 38 | end 39 | end 40 | 41 | defmodule Sendfile do 42 | use ThousandIsland.Handler 43 | 44 | @impl ThousandIsland.Handler 45 | def handle_connection(socket, state) do 46 | ThousandIsland.Socket.sendfile(socket, Path.join(__DIR__, "../support/sendfile"), 0, 6) 47 | ThousandIsland.Socket.sendfile(socket, Path.join(__DIR__, "../support/sendfile"), 1, 3) 48 | send(state[:test_pid], Process.info(self(), :monitored_by)) 49 | {:close, state} 50 | end 51 | end 52 | 53 | defmodule Closer do 54 | use ThousandIsland.Handler 55 | 56 | @impl ThousandIsland.Handler 57 | def handle_connection(_socket, state) do 58 | {:close, state} 59 | end 60 | end 61 | 62 | defmodule Info do 63 | use ThousandIsland.Handler 64 | 65 | @impl ThousandIsland.Handler 66 | def handle_connection(socket, state) do 67 | {:ok, peer_info} = ThousandIsland.Socket.peername(socket) 68 | {:ok, local_info} = ThousandIsland.Socket.sockname(socket) 69 | negotiated_protocol = ThousandIsland.Socket.negotiated_protocol(socket) 70 | connection_information = ThousandIsland.Socket.connection_information(socket) 71 | 72 | ThousandIsland.Socket.send( 73 | socket, 74 | "#{inspect([local_info, peer_info, negotiated_protocol, connection_information])}" 75 | ) 76 | 77 | {:close, state} 78 | end 79 | end 80 | 81 | [:gen_tcp_setup, :ssl_setup] 82 | |> Enum.each(fn setup_fn -> 83 | describe "common behaviour using #{setup_fn}" do 84 | setup setup_fn 85 | 86 | test "should send and receive", context do 87 | {:ok, port} = start_handler(Echo, context.server_opts) 88 | {:ok, client} = context.client_mod.connect(~c"localhost", port, context.client_opts) 89 | 90 | assert context.client_mod.send(client, "HELLO") == :ok 91 | assert context.client_mod.recv(client, 0) == {:ok, ~c"HELLO"} 92 | end 93 | 94 | test "it should close connections", context do 95 | {:ok, port} = start_handler(Closer, context.server_opts) 96 | {:ok, client} = context.client_mod.connect(~c"localhost", port, context.client_opts) 97 | 98 | assert context.client_mod.recv(client, 0) == {:error, :closed} 99 | end 100 | 101 | test "it should emit telemetry events as expected", context do 102 | TelemetryHelpers.attach_all_events(Echo) 103 | 104 | {:ok, port} = start_handler(Echo, context.server_opts) 105 | {:ok, client} = context.client_mod.connect(~c"localhost", port, context.client_opts) 106 | 107 | :ok = context.client_mod.send(client, "HELLO") 108 | {:ok, ~c"HELLO"} = context.client_mod.recv(client, 0) 109 | context.client_mod.close(client) 110 | 111 | assert_receive {:telemetry, [:thousand_island, :connection, :recv], measurements, 112 | metadata}, 113 | 500 114 | 115 | assert measurements ~> %{data: "HELLO"} 116 | assert metadata ~> %{handler: Echo, telemetry_span_context: reference()} 117 | 118 | assert_receive {:telemetry, [:thousand_island, :connection, :send], measurements, 119 | metadata}, 120 | 500 121 | 122 | assert measurements ~> %{data: "HELLO"} 123 | assert metadata ~> %{handler: Echo, telemetry_span_context: reference()} 124 | end 125 | end 126 | end) 127 | 128 | describe "behaviour specific to gen_tcp" do 129 | setup :gen_tcp_setup 130 | 131 | test "it should provide correct connection info", context do 132 | {:ok, port} = start_handler(Info, context.server_opts) 133 | {:ok, client} = context.client_mod.connect(~c"localhost", port, context.client_opts) 134 | {:ok, resp} = context.client_mod.recv(client, 0) 135 | {:ok, local_port} = :inet.port(client) 136 | 137 | expected = 138 | inspect([ 139 | {{127, 0, 0, 1}, port}, 140 | {{127, 0, 0, 1}, local_port}, 141 | {:error, :protocol_not_negotiated}, 142 | {:error, :not_secure} 143 | ]) 144 | 145 | assert to_string(resp) == expected 146 | 147 | context.client_mod.close(client) 148 | end 149 | 150 | test "it should send files", context do 151 | server_opts = Keyword.put(context.server_opts, :handler_options, test_pid: self()) 152 | {:ok, port} = start_handler(Sendfile, server_opts) 153 | {:ok, client} = context.client_mod.connect(~c"localhost", port, context.client_opts) 154 | assert context.client_mod.recv(client, 9) == {:ok, ~c"ABCDEFBCD"} 155 | assert_receive {:monitored_by, []} 156 | end 157 | end 158 | 159 | describe "behaviour specific to ssl" do 160 | setup :ssl_setup 161 | 162 | test "it should provide correct connection info", context do 163 | {:ok, port} = start_handler(Info, context.server_opts) 164 | 165 | {:ok, client} = 166 | context.client_mod.connect(~c"localhost", port, 167 | active: false, 168 | verify: :verify_peer, 169 | cacertfile: Path.join(__DIR__, "../support/ca.pem"), 170 | alpn_advertised_protocols: ["foo"] 171 | ) 172 | 173 | {:ok, {_, local_port}} = context.client_mod.sockname(client) 174 | {:ok, resp} = context.client_mod.recv(client, 0) 175 | 176 | # This is a pretty bogus hack but keeps us from having to have test dependencies on JSON 177 | expected_prefix = 178 | inspect([ 179 | {{127, 0, 0, 1}, port}, 180 | {{127, 0, 0, 1}, local_port}, 181 | {:ok, "foo"} 182 | ]) 183 | |> String.trim_trailing("]") 184 | 185 | assert ^expected_prefix <> rest = to_string(resp) 186 | assert rest =~ ~r/protocol/ 187 | assert rest =~ ~r/cipher/ 188 | 189 | context.client_mod.close(client) 190 | end 191 | 192 | test "it should send files", context do 193 | server_opts = Keyword.put(context.server_opts, :handler_options, test_pid: self()) 194 | {:ok, port} = start_handler(Sendfile, server_opts) 195 | {:ok, client} = context.client_mod.connect(~c"localhost", port, context.client_opts) 196 | assert context.client_mod.recv(client, 9) == {:ok, ~c"ABCDEFBCD"} 197 | assert_receive {:monitored_by, [_pid]} 198 | end 199 | end 200 | 201 | defp start_handler(handler, server_args) do 202 | resolved_args = [port: 0, handler_module: handler] ++ server_args 203 | {:ok, server_pid} = start_supervised({ThousandIsland, resolved_args}) 204 | {:ok, {_ip, port}} = ThousandIsland.listener_info(server_pid) 205 | {:ok, port} 206 | end 207 | end 208 | --------------------------------------------------------------------------------