├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── .travis ├── coverage.sh └── publish-docs.sh ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── appveyor.yml ├── bulk.yaml ├── docs ├── .gitignore ├── Makefile ├── _static │ └── swindon.jpg ├── changelog.rst ├── conf.py ├── config │ ├── auth.rst │ ├── handlers.rst │ ├── http-destinations.rst │ ├── index.rst │ ├── ldap.rst │ ├── main.rst │ ├── mixins.rst │ ├── replication.rst │ ├── routing.rst │ └── session-pools.rst ├── example │ ├── .gitignore │ ├── swindon.yaml │ └── vagga.yaml ├── index.rst ├── installation.rst ├── internals │ ├── index.rst │ ├── load_balancing.rst │ ├── request_id.rst │ ├── swindon_load_balancing.svg │ └── traditional_load_balancer.svg ├── messages.png ├── requirements.txt ├── rust_api │ └── swindon │ │ └── index.rst ├── swindon-lattice │ ├── backend.rst │ ├── crdt.rst │ ├── examples.rst │ ├── frontend.rst │ ├── index.rst │ ├── lattices.rst │ ├── upstream.rst │ └── websocket_shutdown_codes.rst └── swindondomain.py ├── example-peer-A.yaml ├── example-peer-B.yaml ├── example.yaml ├── examples ├── linger │ ├── connect.py │ └── swindon.yaml ├── message-board │ ├── README.rst │ ├── messageboard │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── convention.py │ │ ├── main.py │ │ └── swindon.py │ ├── public │ │ ├── index.html │ │ └── js │ │ │ └── messageboard.js │ ├── requirements.txt │ └── swindon.yaml ├── message-board2 │ ├── .gitignore │ ├── README.rst │ ├── messageboard.js │ ├── messageboard │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── convention.py │ │ ├── main.py │ │ └── swindon.py │ ├── package.json │ ├── public │ │ └── index.html │ ├── requirements.txt │ ├── swindon.yaml │ ├── swindon1.yaml │ ├── swindon2.yaml │ └── webpack.config.js ├── multi-user-chat │ ├── .gitignore │ ├── README.rst │ ├── muc │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── chat.py │ │ ├── convention.py │ │ ├── main.py │ │ └── swindon.py │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── requirements.txt │ ├── src │ │ ├── components │ │ │ ├── Chat.js │ │ │ ├── Login.js │ │ │ ├── Room.js │ │ │ ├── SelectRoom.js │ │ │ ├── chat.css │ │ │ └── login.css │ │ ├── index.css │ │ ├── index.js │ │ ├── login.js │ │ ├── logo.svg │ │ ├── render.js │ │ ├── routes.js │ │ └── websocket.js │ └── swindon.yaml ├── multi-user-chat2 │ ├── .gitignore │ ├── README.rst │ ├── muc │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── chat.py │ │ ├── convention.py │ │ ├── main.py │ │ └── swindon.py │ ├── package.json │ ├── public │ │ └── index.html │ ├── requirements.txt │ ├── src │ │ ├── components │ │ │ ├── Chat.js │ │ │ ├── Login.js │ │ │ ├── Room.js │ │ │ ├── SelectRoom.js │ │ │ ├── chat.css │ │ │ └── login.css │ │ ├── index.css │ │ ├── index.js │ │ ├── login.js │ │ ├── logo.svg │ │ ├── render.js │ │ ├── routes.js │ │ └── server.js │ ├── swindon.yaml │ ├── swindon1.yaml │ └── swindon2.yaml └── presence │ ├── .gitignore │ ├── README.rst │ ├── auth.marko │ ├── index.js │ ├── package.json │ ├── presence │ ├── __init__.py │ ├── __main__.py │ ├── convention.py │ ├── main.py │ └── swindon.py │ ├── public │ └── index.html │ ├── requirements.txt │ ├── swindon.yaml │ ├── swindon1.yaml │ ├── swindon2.yaml │ ├── user_list.marko │ └── webpack.config.js ├── public └── websocket.html ├── src ├── authorizers │ ├── mod.rs │ └── source_ip.rs ├── base64.rs ├── chat │ ├── authorize.rs │ ├── backend.rs │ ├── cid.rs │ ├── close_reason.rs │ ├── connection_sender.rs │ ├── content_type.rs │ ├── dispatcher.rs │ ├── error.rs │ ├── inactivity_handler.rs │ ├── listener │ │ ├── codec.rs │ │ ├── inactivity_handler.rs │ │ ├── mod.rs │ │ ├── pools.rs │ │ └── spawn.rs │ ├── message.rs │ ├── mod.rs │ ├── processor │ │ ├── connection.rs │ │ ├── heap.rs │ │ ├── lattice.rs │ │ ├── main.rs │ │ ├── mod.rs │ │ ├── pair.rs │ │ ├── pool.rs │ │ ├── public.rs │ │ ├── session.rs │ │ └── try_iter.rs │ ├── replication │ │ ├── action.rs │ │ ├── client.rs │ │ ├── mod.rs │ │ ├── server.rs │ │ ├── session.rs │ │ └── spawn.rs │ └── tangle_auth.rs ├── config │ ├── authorizers.rs │ ├── chat.rs │ ├── disk.rs │ ├── empty_gif.rs │ ├── fingerprint.rs │ ├── handlers.rs │ ├── http.rs │ ├── http_destinations.rs │ ├── ldap.rs │ ├── listen.rs │ ├── log.rs │ ├── mod.rs │ ├── networks.rs │ ├── proxy.rs │ ├── read.rs │ ├── redirect.rs │ ├── replication.rs │ ├── root.rs │ ├── routing.rs │ ├── self_status.rs │ ├── session_pools.rs │ ├── static_files.rs │ ├── version.rs │ └── visitors.rs ├── default_error_page.html ├── default_error_page.rs ├── dev │ └── mod.rs ├── empty.gif ├── handlers │ ├── empty_gif.rs │ ├── files │ │ ├── common.rs │ │ ├── decode.rs │ │ ├── default_dir_index.html │ │ ├── index.rs │ │ ├── mod.rs │ │ ├── normal.rs │ │ ├── pools.rs │ │ ├── single.rs │ │ └── versioned.rs │ ├── mod.rs │ ├── proxy.rs │ ├── redirect.rs │ ├── self_status.rs │ ├── swindon_chat.rs │ └── websocket_echo.rs ├── http_pools.rs ├── incoming │ ├── authorizer.rs │ ├── debug.rs │ ├── encoder.rs │ ├── handler.rs │ ├── input.rs │ ├── mod.rs │ ├── quick_reply.rs │ └── router.rs ├── intern.rs ├── logging │ ├── context.rs │ ├── http.rs │ ├── lib.rs │ └── mod.rs ├── main-dev.rs ├── main.rs ├── metrics.rs ├── privileges.rs ├── proxy │ ├── backend.rs │ ├── frontend.rs │ ├── mod.rs │ ├── request.rs │ └── response.rs ├── request_id.rs ├── routing.rs ├── runtime.rs ├── startup.rs ├── template.rs └── updater │ └── mod.rs ├── tests ├── 403.html ├── 404.html ├── 500.html ├── README.rst ├── assets │ ├── a+b.txt │ ├── index │ │ └── index.html │ ├── link.txt │ ├── localhost │ │ └── static-w-hostname │ │ │ └── test.txt │ ├── static_file.html │ ├── static_file.txt │ └── test.html ├── auth_test.py ├── base_redirect_test.py ├── config-w-replication.yaml.tpl ├── config.yaml.tpl ├── config_test.py ├── conftest.py ├── empty_gif_test.py ├── hashed │ ├── aa │ │ ├── bbbbbb-a+b.txt │ │ └── bbbbbb-test.html │ └── bb │ │ └── aaaaaa-test.html ├── proxy_test.py ├── pytest.ini ├── replication_test.py ├── requirements.txt ├── single_file_test.py ├── static_test.py ├── strip_www_redirect_test.py ├── swindon_chat_inactivity.py ├── swindon_chat_test.py ├── swindon_lattice_inactivity.py ├── swindon_lattice_test.py ├── versioned_static_test.py └── websocket_echo_test.py └── vagga.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | /.vagga 2 | /docs/_build 3 | /target 4 | /.cargo 5 | __pycache__ 6 | .cache 7 | /dist 8 | /pkg 9 | /tmp 10 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - vagga --version 3 | - vagga _init_storage_dir --allow-multiple swindon 4 | 5 | stages: 6 | - containers 7 | - test 8 | - publish 9 | - cleanup 10 | 11 | build_docs: 12 | stage: containers 13 | only: 14 | - master 15 | script: 16 | - vagga doc 17 | - gitlab-publish docs/_build/html doc.swindon 18 | 19 | cleanup: 20 | stage: cleanup 21 | when: always 22 | script: 23 | - vagga _clean --unused --at-least 1day 24 | 25 | tests: 26 | stage: test 27 | script: 28 | - vagga cargo-test --color=always 29 | 30 | functional-tests: 31 | stage: test 32 | script: 33 | - vagga func-test --color=yes -n auto -v -rsxX 34 | -------------------------------------------------------------------------------- /.travis/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | build_kcov() { 4 | wget -c https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && 5 | tar xzf master.tar.gz && 6 | cd kcov-master && 7 | mkdir build && 8 | cd build && 9 | cmake .. -DCMAKE_INSTALL_PREFIX=~/.local && 10 | make && 11 | make install && 12 | cd ../.. && 13 | export PATH=$PATH:~/.local/bin 14 | rm -r kcov-master 15 | } 16 | 17 | coverage() { 18 | for file in $(find target/debug -maxdepth 1 -name "swindon*-*" -not -name "*-dev" -executable); do 19 | echo "Running ${file}" && 20 | mkdir -p "target/cov/$(basename $file)" && 21 | kcov --include-path=$(pwd) --verify "target/cov/$(basename $file)/" "$file" || exit 1 22 | done 23 | } 24 | -------------------------------------------------------------------------------- /.travis/publish-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | publish_docs() { 4 | pip install sphinx==1.6.2 docutils ghp-import --user && 5 | pip install -r docs/requirements.txt --user && 6 | make html -C docs SPHINXBUILD=~/.local/bin/sphinx-build && 7 | ~/.local/bin/ghp-import -n docs/_build/html && 8 | git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages 9 | } && publish_docs 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "swindon" 4 | version = "0.7.8" 5 | authors = ["a.popravka@smartweb.com.ua", "paul@colomiets.name"] 6 | description = """ 7 | An HTTP edge (frontend) server with smart websockets support 8 | """ 9 | license = "MIT/Apache-2.0" 10 | readme = "README.md" 11 | keywords = ["tokio", "http", "websockets", "server", "web"] 12 | categories = ["asynchronous", "web-programming::http-server"] 13 | homepage = "http://github.com/swindon-rs/swindon" 14 | documentation = "https://swindon-rs.github.io/swindon" 15 | edition = "2018" 16 | 17 | [dependencies] 18 | futures = "0.1.16" 19 | futures-cpupool = "0.1.6" 20 | tokio-core = "0.1.6" 21 | tokio-io = "0.1.0" 22 | quick-error = "2.0.0" 23 | log = "0.4.0" 24 | env_logger = "0.5.0-rc.1" 25 | quire = "0.3.0" 26 | argparse = "0.2.1" 27 | time = "0.1.35" 28 | lazy_static = "1.0.0" 29 | mime_guess = "1.8.0" 30 | http-file-headers = "0.1.6" 31 | httpdate = "0.3.2" 32 | tk-bufstream = "0.3.0" 33 | tk-http = { version="0.3.6", default-features=false, features=["date_header"] } 34 | netbuf = "0.4.0" 35 | byteorder = "1.0.0" 36 | httpbin = "0.3.3" 37 | slab = "0.4.0" 38 | matches = "0.1.4" 39 | assert_matches = "1.0.1" 40 | string-intern = {version="0.1.7", features=["serde"], default-features=false} 41 | rand = "0.4.1" 42 | tk-pool = "0.5.2" 43 | tk-listen = "0.1.0" 44 | abstract-ns = "0.4.1" 45 | ns-router = "0.1.5" 46 | ns-std-threaded = "0.3.0" 47 | libc = "0.2.31" 48 | scoped-tls = "0.1.0" 49 | self-meter-http = "0.4.1" 50 | libcantal = "0.3.2" 51 | serde = { version = "1.0.15", features = ["rc"] } 52 | serde_derive = "1.0.15" 53 | serde_json = "1.0.3" 54 | blake2 = "0.7.0" 55 | digest = "0.7.2" 56 | digest-writer = "0.3.1" 57 | generic-array = "0.9.0" 58 | typenum = "1.9.0" 59 | regex = "0.2.2" 60 | trimmer = "0.3.2" 61 | humantime = "1.0.0" 62 | void = "1.0.0" 63 | async-slot = "0.1.0" 64 | crossbeam = "0.3.0" 65 | owning_ref = "0.3.3" 66 | 67 | [profile.release] 68 | debug = true 69 | 70 | 71 | [[bin]] 72 | name = "swindon" 73 | path = "src/main.rs" 74 | 75 | [[bin]] 76 | name = "swindon-dev" 77 | path = "src/main-dev.rs" 78 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The swindon Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Swindon 2 | ======= 3 | 4 | **Status: Beta** 5 | 6 | [Documentation](https://swindon-rs.github.io/swindon) | 7 | [Github](https://github.com/swindon-rs/swindon) | 8 | [Crate](https://crates.io/crates/swindon) | 9 | [Chat](https://gitter.im/swindon-rs/Lobby) 10 | 11 | A HTTP edge (frontend) server with smart websocket support. 12 | 13 | 14 | License 15 | ======= 16 | 17 | Licensed under either of 18 | 19 | * Apache License, Version 2.0, 20 | (./LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 21 | * MIT license (./LICENSE-MIT or http://opensource.org/licenses/MIT) 22 | at your option. 23 | 24 | Contribution 25 | ------------ 26 | 27 | Unless you explicitly state otherwise, any contribution intentionally 28 | submitted for inclusion in the work by you, as defined in the Apache-2.0 29 | license, shall be dual licensed as above, without any additional terms or 30 | conditions. 31 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2015 2 | 3 | artifacts: 4 | - path: target\debug\swindon.exe 5 | name: debug-binary 6 | - path: target\debug\swindon-dev.exe 7 | name: debug-binary 8 | 9 | environment: 10 | matrix: 11 | 12 | ### MSVC Toolchains ### 13 | 14 | # Stable 64-bit MSVC 15 | - channel: stable 16 | target: x86_64-pc-windows-msvc 17 | # Beta 64-bit MSVC 18 | - channel: beta 19 | target: x86_64-pc-windows-msvc 20 | # Nightly 64-bit MSVC 21 | - channel: nightly 22 | target: x86_64-pc-windows-msvc 23 | 24 | ### GNU Toolchains ### 25 | 26 | # Stable 64-bit GNU 27 | - channel: stable 28 | target: x86_64-pc-windows-gnu 29 | MSYS_BITS: 64 30 | 31 | ### Allowed failures ### 32 | # 33 | matrix: 34 | allow_failures: 35 | - channel: nightly 36 | 37 | ## Install Script ## 38 | 39 | install: 40 | - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 41 | - rustup-init -yv --default-toolchain %channel% --default-host %target% 42 | - set PATH=%PATH%;%USERPROFILE%\.cargo\bin 43 | - if defined MSYS_BITS set PATH=%PATH%;C:\msys64\mingw%MSYS_BITS%\bin 44 | - rustc -vV 45 | - cargo -vV 46 | 47 | build_script: 48 | - cargo build %cargoflags% 49 | 50 | test_script: 51 | - cargo test --verbose %cargoflags% 52 | -------------------------------------------------------------------------------- /bulk.yaml: -------------------------------------------------------------------------------- 1 | minimum-bulk: v0.4.5 2 | 3 | metadata: 4 | name: swindon 5 | short-description: A HTTP/websocket server 6 | long-description: | 7 | A full-featured HTTP server with support of smart websocket proxying. 8 | 9 | repositories: 10 | 11 | # trusty 12 | - kind: debian 13 | suite: trusty 14 | component: swindon 15 | keep-releases: 1 16 | match-version: ^\d+\.\d+\.\d+\+trusty1$ 17 | 18 | - kind: debian 19 | suite: trusty 20 | component: swindon-stable 21 | keep-releases: 1000 22 | match-version: ^\d+\.\d+\.\d+\+trusty1$ 23 | 24 | - kind: debian 25 | suite: trusty 26 | component: swindon-testing 27 | keep-releases: 100 28 | match-version: \+trusty1$ 29 | 30 | # precise 31 | - kind: debian 32 | suite: precise 33 | component: swindon 34 | keep-releases: 1 35 | match-version: ^\d+\.\d+\.\d+\+precise1$ 36 | add-empty-i386-repo: true 37 | 38 | - kind: debian 39 | suite: precise 40 | component: swindon-stable 41 | keep-releases: 1000 42 | match-version: ^\d+\.\d+\.\d+\+precise1$ 43 | add-empty-i386-repo: true 44 | 45 | - kind: debian 46 | suite: precise 47 | component: swindon-testing 48 | keep-releases: 100 49 | match-version: \+precise1$ 50 | add-empty-i386-repo: true 51 | 52 | # xenial 53 | - kind: debian 54 | suite: xenial 55 | component: swindon 56 | keep-releases: 1 57 | match-version: ^\d+\.\d+\.\d+\+xenial1$ 58 | 59 | - kind: debian 60 | suite: xenial 61 | component: swindon-stable 62 | keep-releases: 1000 63 | match-version: ^\d+\.\d+\.\d+\+xenial1$ 64 | 65 | - kind: debian 66 | suite: xenial 67 | component: swindon-testing 68 | keep-releases: 100 69 | match-version: \+xenial1$ 70 | 71 | versions: 72 | 73 | - file: Cargo.toml 74 | block-start: ^\[package\] 75 | block-end: ^\[.*\] 76 | regex: ^version\s*=\s*"(\S+)" 77 | 78 | - file: docs/conf.py 79 | regex: ^version\s*=\s*'(\S+)' 80 | partial-version: ^\d+\.\d+ 81 | 82 | - file: docs/conf.py 83 | regex: ^release\s*=\s*'(\S+)' 84 | 85 | # for more automation we also update the lockfile 86 | 87 | - file: Cargo.lock 88 | block-start: ^name\s*=\s*"swindon" 89 | regex: ^version\s*=\s*"(\S+)" 90 | block-end: ^\[.*\] 91 | 92 | - file: docs/example/vagga.yaml 93 | regex: \bswindon=([\d\.a-z-]+)\b 94 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /docs/_static/swindon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/docs/_static/swindon.jpg -------------------------------------------------------------------------------- /docs/config/index.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | 5 | `Configuration format basics `_. 6 | 7 | 8 | Configuration sections: 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | main 14 | routing 15 | handlers 16 | session-pools 17 | http-destinations 18 | auth 19 | ldap 20 | replication 21 | mixins 22 | -------------------------------------------------------------------------------- /docs/config/ldap.rst: -------------------------------------------------------------------------------- 1 | LDAP Support 2 | ============ 3 | 4 | **LDAP doesn't work yet, this is provisional documentation** 5 | 6 | Swindon has experimental support of LDAP authorization. 7 | 8 | Configuration of LDAP consists of three parts: 9 | 10 | 1. Configuring LDAP destination. This is where addresses and size of connection 11 | pool are configured. 12 | 2. Actual LDAP search and bind requests are configured in ``authorizers`` 13 | section with ``!Ldap`` authorizer. 14 | 3. And the last but least thing is to add authorizer configured at step #2 to 15 | actually handle parts of the site. 16 | 17 | 18 | LDAP Destination 19 | ---------------- 20 | 21 | Currently destination has minimum configuration: 22 | 23 | .. code-block:: yaml 24 | 25 | ldap-destinations: 26 | local-ldap: 27 | addresses: 28 | - localhost:8398 29 | 30 | Options: 31 | 32 | .. opt:: addresses 33 | 34 | A list of addresses to connect to. Currently you must specify also a port, 35 | but we consider using ``SRV`` records in the future. 36 | 37 | Each address may be resolved to a multiple IPs and each API participate in 38 | round-robin on it's own (not the whole hostname). 39 | 40 | 41 | LDAP Authorizer 42 | --------------- 43 | 44 | Next thing is to configure an authorizer. The authorizer is a thing that 45 | picks specific rules for accessing the website. 46 | 47 | Here is an example of authorizer configuration: 48 | 49 | .. code-block:: yaml 50 | 51 | authorizers: 52 | ldap: !Ldap 53 | destination: local-ldap 54 | search-base: dc=users,dc=example,dc=org 55 | login-attribute: uid 56 | password-attibute: userPassword 57 | login-header: X-User-Uid 58 | additional-queries: 59 | X-User-Groups: 60 | search-base: cn=Group,dc=uaprom,dc=org 61 | fetch-attribute: dn 62 | filter: "member=${dn}" 63 | dn-attribute-strip-base: cn=Group,dc=uaprom,dc=org 64 | 65 | Options: 66 | 67 | .. opt:: destination 68 | 69 | Destination LDAP connection pool name (see :ref:`LDAP Destinations`) 70 | 71 | .. opt:: search-base 72 | 73 | Base DN for searching for user 74 | 75 | .. opt:: login-attribute 76 | 77 | The attribute that will be matched against when user is logging in. 78 | 79 | .. opt:: password-attribute 80 | 81 | The password attribute name for authentication. 82 | 83 | .. opt:: login-header 84 | 85 | A header where valid login will be passed when proxying request to a HTTP 86 | destination (when authentication succeeds). 87 | 88 | .. opt:: additional-queries 89 | 90 | Each of this query will be executed for already logged in user and result 91 | of the query will be passed as the header value to the a HTTP destination. 92 | 93 | -------------------------------------------------------------------------------- /docs/config/mixins.rst: -------------------------------------------------------------------------------- 1 | .. _mixins: 2 | 3 | ====== 4 | Mixins 5 | ====== 6 | 7 | Mixin is another file that contains a number of sections from the configuration 8 | file, but only has items prefixed with specified prefix. For example: 9 | 10 | .. code-block:: yaml 11 | 12 | # main.yaml 13 | routing: 14 | app1.example.com: app1-main 15 | app1.example.com/static: app1-static 16 | app1.example.com/app2: app2-main # app2 is mounted in a folder too 17 | app2.example.com: app2-main 18 | app2.example.com/static: app2-empty 19 | 20 | mixins: 21 | app1-: app1.yaml 22 | app2-: app2.yaml 23 | 24 | # app1.yaml 25 | 26 | handlers: 27 | app1-main: !Proxy 28 | destination: app1-service 29 | app1-static: !Static 30 | 31 | http-destination: 32 | app1-service: 33 | addresses: 34 | - localhost:8000 35 | 36 | # app2.yaml 37 | 38 | handlers: 39 | app2-main: !Static 40 | app2-empty: !EmptyGif 41 | 42 | Note the following things: 43 | 44 | 1. Swindon ensures that all handlers, http-destinations, and other things 45 | are prefixed in a file, to avoid mistakes 46 | 2. ``routing`` section is not mixed in. You can split it via 47 | includes_ and merge-tags_ if you want. 48 | 3. You can mix and match different handlers in ``routing`` table as well 49 | as refer to the items accross files. There is no limitation on referencing, 50 | only on definition of items. 51 | 52 | This allows nice splitting and incapsulation in config. Still keeping routing 53 | table short an clean. 54 | 55 | Sections supported for mixins: 56 | 57 | * :sect:`handlers` 58 | * :sect:`authorizers` 59 | * :sect:`session-pools` 60 | * :sect:`http-destinations` 61 | * :sect:`ldap-destinations` 62 | * :sect:`networks` 63 | * :sect:`log-formats` 64 | * :sect:`disk-pools` 65 | 66 | .. _includes: http://rust-quire.readthedocs.io/en/latest/user.html#includes 67 | .. _merge-tags: http://rust-quire.readthedocs.io/en/latest/user.html#merging-mappings 68 | -------------------------------------------------------------------------------- /docs/config/replication.rst: -------------------------------------------------------------------------------- 1 | Replication configuration 2 | ========================= 3 | 4 | Swindon employs a special protocol we call "replication" to keep in sync 5 | swindon instances serving same session pools. This is required to allow users 6 | to connect to any instance and get all updates that target the user, and also 7 | allows backends to send requests and data to any swindon instance without 8 | doing any sophisticated logic. 9 | 10 | 11 | Example 12 | ------- 13 | 14 | .. code-block:: yaml 15 | 16 | replication: 17 | listen: 18 | - 0.0.0.0:7878 19 | 20 | peers: 21 | - peer2:7878 22 | - peer3:7878 23 | 24 | max-connections: 10 25 | listen-error-timeout: 100ms 26 | reconnect-timeout: 5s 27 | 28 | 29 | Options 30 | ------- 31 | 32 | .. opt:: listen 33 | 34 | A list of addresses to bind to. 35 | 36 | .. opt:: peers 37 | 38 | A list of peer names to connect to. 39 | 40 | .. opt:: max-connections 41 | 42 | (default ``10``) Maximum number of client connections to accept. 43 | 44 | .. opt:: listen-error-timeout 45 | 46 | (default ``100ms``) Time to sleep when we caught error accepting connection. 47 | 48 | .. opt:: reconnect-timeout 49 | 50 | (default ``5s``) Time to sleep between retrying to connect to peer. 51 | -------------------------------------------------------------------------------- /docs/config/routing.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: yaml 2 | 3 | .. _routing: 4 | 5 | Routing Table 6 | ============= 7 | 8 | Describes routing table for all input requests. 9 | It is a mapping of either exact host and path prefix, 10 | or host suffix and path path prefix 11 | to the name of the :ref:`handler `. 12 | 13 | Example of routing table:: 14 | 15 | routing: 16 | localhost/empty.gif: empty-gif 17 | localhost/admin: admin-handler 18 | localhost/js: static-files 19 | localhost/: proxy-handler 20 | "*.example.com/": all-subdomains-handler 21 | www.example.com/favicon.ico: favicon-handler 22 | 23 | Route rosolution is done in two steps: 24 | 25 | 1. The host is matched first: 26 | 27 | a) exact match is tested first (``www.example.com``), 28 | 29 | b) then match by suffix is checked (``*.example.com``). 30 | 31 | 2. The path prefix within that host is matched. 32 | 33 | Here is the example for route matching: 34 | 35 | Assume we requested ``www.example.com/hello`` URL, 36 | at first step the host ``www.example.com`` will be matched 37 | with last entry in table above, next, path ``/hello`` will 38 | be tested against all pathes for that host -- only one in our case -- 39 | and ``/favicon.ico`` path doesn't match ``/hello``. 40 | So the request for ``www.example.com/hello`` will end up with ``404 Not Found``. 41 | -------------------------------------------------------------------------------- /docs/config/session-pools.rst: -------------------------------------------------------------------------------- 1 | .. _sessions: 2 | 3 | .. highlight:: yaml 4 | 5 | ============ 6 | Session Pool 7 | ============ 8 | 9 | 10 | Session pool is a fully-isolated namespace of swindon chat service with 11 | it's own address for backend connections. Each client websocket can connect 12 | to exactly one session pool. 13 | 14 | Note: while it's tempting to use a session pool per application, it may or 15 | may not make sense for your specific case. You may combine multiple 16 | "applications" under umbrella of a single session pool to connect all of them 17 | using a single websocket. Each session pool contains multiple namespaces of 18 | "lattices" and you can arbitrarily nest pub-sub topics, so there are plenty 19 | room for isolating and integrating multiple applications in the session 20 | pool. 21 | 22 | 23 | Example 24 | ======= 25 | 26 | .. code-block:: yaml 27 | 28 | session-pools: 29 | 30 | example-chat-session: 31 | listen: 32 | - 127.0.0.1:2007 33 | inactivity-handlers: 34 | - some-destination/chat/route 35 | 36 | 37 | Options 38 | ======= 39 | 40 | .. opt:: listen 41 | 42 | List of sockets to listen to and accept connections 43 | 44 | Example:: 45 | 46 | listen: 47 | - 127.0.0.1:2222 48 | - 127.0.0.1:3333 49 | 50 | .. opt:: max-connections 51 | 52 | (default ``1000``) Maximum number of backend connections to accept. Note 53 | you should bump up a file descriptor limit to something larger than this 54 | value + max client connections. 55 | 56 | .. opt:: max-payload-size 57 | 58 | (default ``10Mib``) Maximum size of the payload (json data) from backend 59 | to swindon. 60 | 61 | .. opt:: pipeline-depth 62 | 63 | (default ``2``) Accept maximum N in-flight requests for each HTTP 64 | connection. Pipelined requests improve performance of your service but also 65 | expose it to DoS attacks. 66 | 67 | .. opt:: listen-error-timeout 68 | 69 | (default ``100ms``) Time to sleep when we caught error accepting connection, 70 | mostly error is some resource shortage (usually EMFILE or ENFILE), so 71 | repeating after some short timeout makes sense (chances that some connection 72 | freed some resources). 73 | 74 | 75 | .. opt:: inactivity-handlers 76 | TBD 77 | 78 | -------------------------------------------------------------------------------- /docs/example/.gitignore: -------------------------------------------------------------------------------- 1 | /.vagga 2 | -------------------------------------------------------------------------------- /docs/example/swindon.yaml: -------------------------------------------------------------------------------- 1 | listen: 2 | - 127.0.0.1:8080 3 | 4 | routing: 5 | localhost/js: public 6 | localhost/css: public 7 | localhost/img: public 8 | 9 | handlers: 10 | public: !Static 11 | mode: relative_to_domain_root 12 | index-files: [index.html] 13 | path: public 14 | text-charset: utf-8 15 | -------------------------------------------------------------------------------- /docs/example/vagga.yaml: -------------------------------------------------------------------------------- 1 | containers: 2 | swindon: 3 | setup: 4 | - !Ubuntu xenial 5 | - !UbuntuRepo 6 | url: https://repo.mglawica.org/ubuntu/ 7 | suite: xenial 8 | components: [swindon-stable] 9 | trusted: true 10 | - !Install [swindon=0.7.8+xenial1] 11 | 12 | commands: 13 | swindon: !Command 14 | container: swindon 15 | run: 16 | - swindon 17 | - --verbose 18 | - --config=swindon.yaml 19 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Swindon's documentation! 2 | =================================== 3 | 4 | 5 | Swindon is a web server that eventually should develop all the features needed 6 | for a frontend server. But the most powerful feature is a 7 | :ref:`protocol ` for handling websockets. 8 | 9 | Github_ | Crate_ 10 | 11 | 12 | .. figure:: messages.png 13 | :alt: a screenshot of a dashboard showing a 122k simultaneous connections 14 | and 22.6M messages in a day 15 | 16 | *While swindon is quite recent project it handles about 120k simultaneous 17 | connections and ~20 million messages per day in our setup. The screenshot 18 | above shows just a random day from our dashboard.* 19 | 20 | 21 | Contents: 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | 26 | installation 27 | config/index 28 | internals/index 29 | swindon-lattice/index 30 | changelog 31 | 32 | .. _github: https://github.com/swindon-rs/swindon 33 | .. _crate: https://crates.io/crates/swindon 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`search` 40 | 41 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | .. contents:: 6 | 7 | 8 | Using Cargo 9 | =========== 10 | 11 | You can install *swindon* using cargo:: 12 | 13 | cargo install swindon 14 | 15 | But we also provide binaries for ubuntu and vagga_ configs. 16 | 17 | 18 | Example Config 19 | ============== 20 | 21 | To run swindon you need some config here is the minimal one serving static 22 | from ``public`` folder at port 8080: 23 | 24 | 25 | .. literalinclude:: example/swindon.yaml 26 | :language: yaml 27 | 28 | 29 | Ubuntu Installation 30 | =================== 31 | 32 | Installation for ubuntu xenial:: 33 | 34 | echo 'deb [trusted=yes] http://repo.mglawica.org/ubuntu/ xenial swindon' | sudo tee /etc/apt/sources.list.d/swindon.list 35 | apt-get update 36 | apt-get install swindon 37 | 38 | 39 | More repositories:: 40 | 41 | deb [trusted=yes] http://repo.mglawica.org/ubuntu/ xenial swindon 42 | deb [trusted=yes] http://repo.mglawica.org/ubuntu/ xenial swindon-testing 43 | deb [trusted=yes] http://repo.mglawica.org/ubuntu/ precise swindon 44 | deb [trusted=yes] http://repo.mglawica.org/ubuntu/ precise swindon-testing 45 | deb [trusted=yes] http://repo.mglawica.org/ubuntu/ trusty swindon 46 | deb [trusted=yes] http://repo.mglawica.org/ubuntu/ trusty swindon-testing 47 | 48 | 49 | Vagga Installation 50 | ================== 51 | 52 | Same as above, but in form of vagga config: 53 | 54 | .. literalinclude:: example/vagga.yaml 55 | 56 | .. _vagga: http://vagga.readthedocs.org 57 | -------------------------------------------------------------------------------- /docs/internals/index.rst: -------------------------------------------------------------------------------- 1 | Swindon Internals 2 | ================= 3 | 4 | This section documents swindon internals. Many of them are visible in some way 5 | to developers and ops using swindon so we document them here. 6 | 7 | Contents: 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | request_id 13 | load_balancing 14 | -------------------------------------------------------------------------------- /docs/internals/load_balancing.rst: -------------------------------------------------------------------------------- 1 | Load Balancing 2 | ============== 3 | 4 | This is an informal description of how swindon does load balancing. Note 5 | currently there is only one strategy described here. We have few improved 6 | strategies on the roadmap. 7 | 8 | To make it easier to reason about, let's start from describing traditional 9 | load balancers (this is **not** how swindon works): 10 | 11 | .. image:: traditional_load_balancer.svg 12 | 13 | This is how TCP level load-balancers work, and also how HAProxy and Nginx 14 | work to the best of my knowledge. (Nginx can offload keep-alive connections 15 | on itself, but if we change "100 Active Connections" to "100 Active Requests", 16 | chart is he same for nginx goo). 17 | 18 | Here is how swindon works: 19 | 20 | .. image:: swindon_load_balancing.svg 21 | 22 | More specifically: 23 | 24 | 1. Opens at max N connections to each backend 25 | (:opt:`backend-coonnections-per-ip-port`) 26 | 2. Queues requests internally 27 | 3. Sends queued requests to first server become idle 28 | 29 | .. note:: The picture describes a concept but the default settings are 30 | different. Default connection limit is 100 and we don't enable pipelining 31 | by default (because it's unsafe to pipeline POST requests or long-polling). 32 | -------------------------------------------------------------------------------- /docs/internals/request_id.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Request Id 3 | ========== 4 | 5 | Currently we have a single algorithm for generating unique request identifiers, 6 | in future we may add other ones if they are required. 7 | 8 | 99 percent of the time you should treat request_ids as opaque strings, so 9 | this documentation is just for your info. 10 | 11 | Generation 12 | ========== 13 | 14 | Request id as generated by swindon is 32bytes of url-safe base64 chars 15 | (alphanumeric characters + ``-`` and ``_``, no padding as we fit base64 16 | exactly). 17 | 18 | Request id is generated from 24 bytes of data consisting of three parts: 19 | 20 | 1. Current timestamp in milliseconds, wrapped at 6 bytes 21 | (will wrap in 8k years) 22 | 2. 12 bytes of random data, which are thread id which accepted thread. These 23 | bytes are not synchronized between threads and workers, but it's assumed 24 | that 96 bits of random data is enough to have clashes improbable. 25 | 3. A per-thread sequence number of request id which wraps at 6 bytes 26 | 27 | As you can see you can guess the timestamp from the request id, and request 28 | id's are also k-ordered, i.e. you can sort them alphanumerically and get 29 | estimation of sequence of running requests. 30 | 31 | 32 | Request Id Propagation 33 | ====================== 34 | 35 | Sometimes you may want to get request id from another system, this is on 36 | our to do list. 37 | 38 | 39 | Q & A 40 | ===== 41 | 42 | **Why request-id is not an uuid4?** 43 | 44 | There are few reasons: 45 | 46 | * Because usually uuid4 is fetched from good random generator source, but 47 | we will quickly drain system's random generator if we use it for every 48 | request id. Using PRNG for the task is possible, but that approach doesn't 49 | look like superior to what we have now. 50 | * It's good to have k-ordered request ids and to know timestamp when request 51 | id was generated. Thread id and number may provide additional aid in 52 | debugging few kinds of issues (like server crashes and recovery) 53 | * Also our request-ids are shorter, while providing additional info 54 | 55 | Downsides: 56 | 57 | * Request id provides some info that may be useful for attacker to do some 58 | harm. So you should **not** expose request ids to users. 59 | -------------------------------------------------------------------------------- /docs/messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/docs/messages.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinxcontrib-domaintools==0.1 2 | sphinxcontrib-httpdomain 3 | sphinx_rtd_theme 4 | setuptools 5 | -------------------------------------------------------------------------------- /docs/rust_api/swindon/index.rst: -------------------------------------------------------------------------------- 1 | Swindon Crate docs 2 | ================== 3 | -------------------------------------------------------------------------------- /docs/swindon-lattice/crdt.rst: -------------------------------------------------------------------------------- 1 | .. _crdt-types: 2 | 3 | ========== 4 | CRDT Types 5 | ========== 6 | 7 | Data types that are called CRDT which is either Conflict-free Replicated Data Type or Commutative Replicated Data Types are used in 8 | :ref:`lattices `. 9 | 10 | Available types: 11 | 12 | .. crdt:: counter 13 | 14 | ever-increasing counter 15 | 16 | .. code-block:: javascript 17 | 18 | // some initial value 19 | {"user_visits_counter": 1} 20 | // next value 21 | {"user_visits_counter": 2} 22 | 23 | // on update this value will be ignored 24 | {"user_visits_counter": 1} 25 | 26 | .. crdt:: set 27 | 28 | set of some elements, the set can only grow (new elements added) and 29 | elements can never be deleted from the set 30 | 31 | .. code-block:: javascript 32 | 33 | {"last_message_set": [1, 12, 13465]} 34 | 35 | // on update -- will have no effect 36 | {"last_message_set": [1, 12]} 37 | 38 | 39 | .. _register-crdt: 40 | 41 | .. crdt:: register 42 | 43 | a value with a timestamp or a version, which is updated with 44 | last-write-wins (LWW) semantics. Any valid json might be used as a value. 45 | 46 | .. code-block:: javascript 47 | 48 | {"status_register": [1, {"icon": "busy", "message": "working"}]} 49 | // next value 50 | {"status_register": [2, {"icon": "ready_for_chat", "message": "???"}]} 51 | 52 | // on update -- will have no effect 53 | {"status_register": [1, {"icon": "offline", "message": "disconnected"}]} 54 | 55 | {"another_register": [1503339804.859186, "hello_string"]} 56 | 57 | .. note:: Only non-negative values might be used as a version. Both 58 | integers and floating point number are okay, but value will be treated 59 | -------------------------------------------------------------------------------- /docs/swindon-lattice/examples.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Example Applications 3 | ==================== 4 | 5 | There are examples in the swindon repository, which you might want to 6 | study to better understand the underlying protocols. There are four 7 | examples: 8 | 9 | 1. `message-board`_ -- displays a list of messages from every user joined 10 | the chat, using pub-sub. Does not represent all the details of the protocol 11 | but it has < 60 lines of raw javascript code without any libraries, so 12 | it's easy to grok for people with different backgrounds. Backend is in 13 | python3_ and sanic_ (for some coincidence) 14 | 2. `message-board2`_ -- basically the same but uses a `swindon-js`_ helper 15 | library for the frontend. And `aiohttp.web`_ for backend. 16 | 3. `multi-user-chat`_ -- is a more powerful chat application with rooms and 17 | using both lattices and pubsub for keeping state up to date. Uses 18 | `create-react-app`_ for bootstrapping the application and sanic sanic_ for 19 | backend. 20 | 4. `multi-user-chat2`_ -- is basically the same thing, but uses `swindon-js`_ 21 | library for communicating with swindon. 22 | 23 | .. _message-board: https://github.com/swindon-rs/swindon/tree/master/examples/message-board 24 | .. _message-board2: https://github.com/swindon-rs/swindon/tree/master/examples/message-board2 25 | .. _multi-user-chat: https://github.com/swindon-rs/swindon/tree/master/examples/multi-user-chat 26 | .. _multi-user-chat2: https://github.com/swindon-rs/swindon/tree/master/examples/multi-user-chat2 27 | .. _python3: http://python.org 28 | .. _sanic: https://github.com/channelcat/sanic/ 29 | .. _aiohttp.web: http://aiohttp.readthedocs.io/ 30 | .. _swindon-js: https://npmjs.com/package/swindon 31 | .. _create-react-app: https://github.com/facebookincubator/create-react-app 32 | -------------------------------------------------------------------------------- /docs/swindon-lattice/index.rst: -------------------------------------------------------------------------------- 1 | .. _swindon-lattice: 2 | 3 | Swindon-lattice Protocol 4 | ======================== 5 | 6 | The protocol is higher level-protocol on top of websockets that allows routing 7 | messages between backends, push from any backend to any user, publish-subscribe 8 | and few other useful things. 9 | 10 | The protocol is designed to cover large number of cases including different 11 | applications covered by single websocket connection for efficiency. 12 | 13 | The protocol is `registered by IANA `_ as ``v1.swindon-lattice+json`` 14 | and this value needs to be passed in ``Sec-WebSocket-Protocol`` field in 15 | handshake. 16 | 17 | .. toctree:: 18 | 19 | lattices 20 | crdt 21 | frontend 22 | upstream 23 | backend 24 | websocket_shutdown_codes 25 | examples 26 | 27 | .. _iana_ws: https://www.iana.org/assignments/websocket/websocket.xhtml 28 | -------------------------------------------------------------------------------- /docs/swindon-lattice/websocket_shutdown_codes.rst: -------------------------------------------------------------------------------- 1 | .. _websocket-shutdown-codes: 2 | 3 | Websocket Shutdown Codes 4 | ======================== 5 | 6 | Here are codes that you can see in ``onclose`` event handler: 7 | 8 | .. code-block:: javascript 9 | 10 | var ws = new WebSocket(..) 11 | os.onclose = function(ev) { 12 | console.log(ev.code, ev.reason) 13 | } 14 | 15 | .. note:: Chromium doesn't report custom shutdown codes to Javascript. So 16 | we also duplicate same erorr using :ref:`fatal_error message`. 17 | 18 | Our custom codes (and reasons): 19 | 20 | * ``4001``, ``session_pool_stopped`` -- session pool is closed, 21 | basically this means that this specific 22 | application is not supported by this server any more. This message may be 23 | received at any time. 24 | * ``4400``, ``backend_error`` -- no websockets allowed at this route 25 | * ``4401``, ``backend_error`` -- unauthorized (i.e. no cookie or other 26 | authentication data) 27 | * ``4403``, ``backend_error`` -- forbidden (i.e. authorized but not allowed 28 | * ``4404``, ``backend_error`` -- route not found (http status: Not Found) 29 | * ``4410``, ``backend_error`` -- route not found (http status: Gone) 30 | * ``4500``, ``backend_error`` -- internal server error when authorizing 31 | * ``4503``, ``backend_error`` -- temporary error, unable to authorize 32 | 33 | Bacically we have reserved these status codes to correspond to HTTP error 34 | codes returned from backend. But we only guarantee to propagate codes 35 | described above, because other ones may impose security vulnerability. 36 | 37 | * ``4400-4499``, ``backend_error`` -- for HTTP codes 400-499 38 | * ``4500-4599``, ``backend_error`` -- for HTTP codes 500-599 39 | 40 | These errors only propagate on connection authorization. When single request 41 | fails we respond with ``["error"...]`` as websocket message. 42 | -------------------------------------------------------------------------------- /docs/swindondomain.py: -------------------------------------------------------------------------------- 1 | from sphinx import addnodes 2 | from sphinx.util import ws_re 3 | from sphinx.directives import ObjectDescription 4 | from sphinxcontrib.domaintools import custom_domain 5 | 6 | 7 | def setup(app): 8 | app.add_domain(custom_domain( 9 | 'SwindonConfig', 10 | name='swindon', 11 | label="Swindon Config", 12 | elements=dict( 13 | opt=dict( 14 | objname="Configuration Option", 15 | indextemplate="pair: %s; Config Option", 16 | domain_object_class=GenericObject, 17 | ), 18 | sect=dict( 19 | objname="Configuration Section", 20 | indextemplate="pair: %s; Config Section", 21 | domain_object_class=GenericObject, 22 | ), 23 | handler=dict( 24 | objname="Handler", 25 | indextemplate="pair: %s; Request Handler", 26 | domain_object_class=GenericObject, 27 | ), 28 | crdt=dict( 29 | objname="CRDT Type", 30 | indextemplate="pair: %s; CRDT Type", 31 | domain_object_class=GenericObject, 32 | ), 33 | ))) 34 | 35 | 36 | class GenericObject(ObjectDescription): 37 | """ 38 | A generic x-ref directive registered with Sphinx.add_object_type(). 39 | """ 40 | indextemplate = '' 41 | parse_node = None 42 | 43 | def handle_signature(self, sig, signode): 44 | if self.parse_node: 45 | name = self.parse_node(self.env, sig, signode) 46 | else: 47 | signode.clear() 48 | signode += addnodes.desc_name(sig, sig) 49 | # normalize whitespace like XRefRole does 50 | name = ws_re.sub('', sig) 51 | return name 52 | 53 | def add_target_and_index(self, name, sig, signode): 54 | targetname = '%s-%s' % (self.objtype, name) 55 | signode['ids'].append(targetname) 56 | self.state.document.note_explicit_target(signode) 57 | if self.indextemplate: 58 | colon = self.indextemplate.find(':') 59 | if colon != -1: 60 | indextype = self.indextemplate[:colon].strip() 61 | indexentry = self.indextemplate[colon + 1:].strip() % (name,) 62 | else: 63 | indextype = 'single' 64 | indexentry = self.indextemplate % (name,) 65 | self.indexnode['entries'].append((indextype, indexentry, 66 | targetname, '', None)) 67 | # XXX: the only part changed is domain: 68 | self.env.domaindata['swindon']['objects'][self.objtype, name] = \ 69 | self.env.docname, targetname 70 | -------------------------------------------------------------------------------- /examples/linger/connect.py: -------------------------------------------------------------------------------- 1 | import random 2 | import socket 3 | import argparse 4 | import time 5 | 6 | 7 | def main(): 8 | ap = argparse.ArgumentParser() 9 | ap.add_argument("-n", "--connections", type=int, default=1000) 10 | ap.add_argument("--ips", type=int, default=100) 11 | opt = ap.parse_args() 12 | sockets = [] 13 | for i in range(opt.connections): 14 | ip = "127.0.0.{}".format(random.randint(1, 100)) 15 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 16 | s.connect((ip, 8080)) 17 | sockets.append(s) 18 | if (i+1) % 1000 == 0: 19 | print(i+1) 20 | print("Done") 21 | while True: 22 | time.sleep(1000) 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /examples/message-board/README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Message Board Example 3 | ===================== 4 | 5 | This is a minimal message board a/k/a simple chat example using 6 | 7 | * `python3 `_ 8 | * `sanic `_ 9 | * Raw script in javascript with no libraries and build tools 10 | 11 | -------------------------------------------------------------------------------- /examples/message-board/messageboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/examples/message-board/messageboard/__init__.py -------------------------------------------------------------------------------- /examples/message-board/messageboard/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | from .main import main 3 | main() 4 | -------------------------------------------------------------------------------- /examples/message-board/messageboard/convention.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | from functools import wraps 5 | 6 | from sanic.response import json as response 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | class User(object): 11 | 12 | def __init__(self, user_id): 13 | self.user_id = user_id 14 | # this is a hack to get rid of DB 15 | self.username = user_id.replace('_', ' ').title() 16 | 17 | def __repr__(self): 18 | return "".format(self.user_id) 19 | 20 | 21 | class Connection(object): 22 | 23 | def __init__(self, connection_id): 24 | self.connection_id = connection_id 25 | 26 | def __repr__(self): 27 | return "".format(self.connection_id) 28 | 29 | 30 | class Request(object): 31 | 32 | def __init__(self, auth, *, request_id=None, connection_id, **_unused): 33 | self.request_id = request_id 34 | self.connection = Connection(connection_id) 35 | if auth: 36 | kind, value = auth.split(' ') 37 | assert kind == 'Tangle' 38 | auth = json.loads( 39 | base64.b64decode(value.encode('ascii')).decode('utf-8')) 40 | self.user = User(**auth) 41 | 42 | def __repr__(self): 43 | return "".format( 44 | getattr(self, 'user', self.connection)) 45 | 46 | 47 | 48 | def swindon_convention(f): 49 | @wraps(f) 50 | async def swindon_call_method(request): 51 | req = None 52 | try: 53 | metadata, args, kwargs = request.json 54 | req = Request(request.headers.get("Authorization"), **metadata) 55 | result = await f(req, *args, **kwargs) 56 | return response(result) 57 | except Exception as e: 58 | log.exception("Error for %r", req or request, exc_info=e) 59 | raise 60 | return swindon_call_method 61 | 62 | -------------------------------------------------------------------------------- /examples/message-board/messageboard/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import uvloop 4 | import argparse 5 | from censusname import generate as make_name 6 | from sanic import Sanic 7 | from sanic.response import json as response 8 | 9 | from .convention import swindon_convention 10 | from .swindon import connect 11 | 12 | def main(): 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument('--port', default=8082, help="Listen port") 15 | ap.add_argument('--swindon-port', default=8081, 16 | help="Connect to swindon at port") 17 | options = ap.parse_args() 18 | 19 | logging.basicConfig(level=logging.DEBUG) 20 | loop = uvloop.new_event_loop() 21 | app = Sanic('messageboard') 22 | swindon = connect(('localhost', options.swindon_port), loop=loop) 23 | 24 | @app.route("/tangle/authorize_connection", methods=['POST']) 25 | @swindon_convention 26 | async def auth(req, http_authorization, http_cookie, url_querystring): 27 | name = make_name() 28 | id = name.lower().replace(' ', '_') 29 | await swindon.subscribe(req.connection, 'message-board') 30 | return { 31 | 'user_id': id, 32 | 'username': name, 33 | } 34 | 35 | @app.route("/message", methods=['POST']) 36 | @swindon_convention 37 | async def message(req, text): 38 | await swindon.publish('message-board', { 39 | 'author': req.user.username, 40 | 'text': text, 41 | }) 42 | return True 43 | 44 | 45 | if os.environ.get("LISTEN_FDS") == '1': 46 | # Systemd socket activation protocol 47 | import socket 48 | sock = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) 49 | app.run(host=None, port=None, sock=sock, loop=loop) 50 | else: 51 | app.run(host="0.0.0.0", port=options.port, loop=loop) 52 | -------------------------------------------------------------------------------- /examples/message-board/messageboard/swindon.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import aiohttp 4 | 5 | TOPIC_RE = re.compile("^[a-zA-Z0-9_-].") 6 | 7 | 8 | class Swindon(object): 9 | 10 | def __init__(self, addr, loop): 11 | self.addr = addr 12 | self.prefix = 'http://{}:{}/v1/'.format(*self.addr) 13 | self.session = aiohttp.ClientSession(loop=loop) 14 | 15 | async def subscribe(self, conn, topic): 16 | assert TOPIC_RE.match(topic) 17 | async with self.session.put(self.prefix + 18 | 'connection/{}/subscriptions/{}'.format( 19 | conn.connection_id, 20 | topic), 21 | data='') as req: 22 | await req.read() 23 | 24 | 25 | async def publish(self, topic, data): 26 | assert TOPIC_RE.match(topic) 27 | async with self.session.post(self.prefix + 'publish/' + topic, 28 | data=json.dumps(data)) as req: 29 | await req.read() 30 | 31 | 32 | def connect(addr, loop): 33 | return Swindon(addr, loop=loop) 34 | 35 | -------------------------------------------------------------------------------- /examples/message-board/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Hello, and welcome to the cranky messageboard!

8 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/message-board/public/js/messageboard.js: -------------------------------------------------------------------------------- 1 | +function() { 2 | 3 | var ws = new WebSocket("ws://" + location.host + "/", 4 | ["v1.swindon-lattice+json"]) 5 | var mb = document.getElementById('mb'); 6 | var input = document.getElementById('input'); 7 | var my_user_id = null; 8 | ws.onopen = function() { 9 | log('debug', "Connected") 10 | } 11 | 12 | ws.onclose = function() { 13 | input.style.visibility = 'hidden' 14 | log('warning', "Disconnected") 15 | } 16 | 17 | ws.onerror = function(e) { 18 | input.style.visibility = 'hidden' 19 | log('warning', 'ERROR: ' + e) 20 | } 21 | ws.onmessage = function(ev) { 22 | var data = JSON.parse(ev.data) 23 | switch(data[0]) { 24 | case 'hello': 25 | my_user_id = data[2]['user_id'] 26 | log('info', "Your name is " + data[2]['username']) 27 | input.style.visibility = 'visible' 28 | input.focus() 29 | break; 30 | case 'message': 31 | if(data[1].topic == 'message-board') { 32 | var author = data[2]['author'] 33 | var text = data[2]['text'] 34 | log('text', "[" + author + "] " + text) 35 | break; 36 | } 37 | default: 38 | console.error("Skipping message", data) 39 | } 40 | } 41 | input.onkeydown = function(ev) { 42 | if(ev.which == 13) { 43 | ws.send(JSON.stringify([ 44 | "message", // method 45 | {'request_id': 1}, // metadata 46 | [input.value], // args 47 | {}, // kwargs 48 | ])) 49 | input.value = '' 50 | } 51 | } 52 | 53 | 54 | function log(type, message) { 55 | let red = document.createElement('div'); 56 | red.className = type; 57 | red.appendChild(document.createTextNode(message)); 58 | mb.insertBefore(red, mb.childNodes[0]); 59 | } 60 | 61 | }() 62 | -------------------------------------------------------------------------------- /examples/message-board/requirements.txt: -------------------------------------------------------------------------------- 1 | sanic==0.1.7 2 | aiohttp==1.1.5 3 | censusname==0.2.2 4 | -------------------------------------------------------------------------------- /examples/message-board/swindon.yaml: -------------------------------------------------------------------------------- 1 | 2 | listen: 3 | - 127.0.0.1:8080 4 | 5 | debug-routing: true 6 | 7 | routing: 8 | localhost/empty.gif: empty-gif 9 | localhost/js: public 10 | localhost/css: public 11 | localhost: chat 12 | localhost/~~swindon-status: status 13 | 14 | handlers: 15 | 16 | chat: !SwindonLattice 17 | 18 | session-pool: chat 19 | http-route: html 20 | compatibility: v0.6.2 21 | 22 | message-handlers: 23 | "*": chat 24 | 25 | empty-gif: !EmptyGif 26 | status: !SelfStatus 27 | 28 | public: !Static 29 | mode: relative_to_domain_root 30 | path: ./public 31 | text-charset: utf-8 32 | 33 | html: !SingleFile 34 | path: ./public/index.html 35 | content-type: "text/html; charset=utf-8" 36 | 37 | 38 | session-pools: 39 | chat: 40 | listen: [127.0.0.1:8081] 41 | inactivity-handlers: [chat] 42 | 43 | 44 | http-destinations: 45 | chat: 46 | override-host-header: swindon.internal 47 | addresses: 48 | - 127.0.0.1:8082 49 | -------------------------------------------------------------------------------- /examples/message-board2/.gitignore: -------------------------------------------------------------------------------- 1 | /public/js 2 | -------------------------------------------------------------------------------- /examples/message-board2/README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Message Board Example 3 | ===================== 4 | 5 | This is a minimal message board a/k/a simple chat example using 6 | 7 | * `python3 `_ 8 | * `aiohttp.web `_ 9 | * `swindon-js `_ 10 | * `webpack `_ 11 | 12 | -------------------------------------------------------------------------------- /examples/message-board2/messageboard.js: -------------------------------------------------------------------------------- 1 | import { Swindon } from 'swindon' 2 | 3 | var swindon = new Swindon("ws://"+location.host, { 4 | onStateChange: update_status, 5 | }) 6 | var mb = document.getElementById('mb'); 7 | var status_el = document.getElementById('status_text'); 8 | var input = document.getElementById('input'); 9 | var my_user_id = null; 10 | 11 | input.onkeydown = function(ev) { 12 | if(ev.which == 13) { 13 | swindon.call( 14 | "message", // method 15 | [input.value], // args 16 | {}, // kwargs 17 | ) 18 | input.value = '' 19 | } 20 | } 21 | 22 | start_chat() 23 | // interval helps to update counter of seconds to reconnect 24 | setInterval(() => update_status(swindon.state()), 1000) 25 | 26 | async function start_chat() { 27 | swindon.guard().listen("message-board", add_message) 28 | let user_info = await swindon.waitConnected() 29 | log('info', "Your name is " + user_info['username']) 30 | input.style.visibility = 'visible' 31 | input.focus() 32 | } 33 | 34 | function update_status(state) { 35 | console.log("Websocket status changed", state) 36 | switch(state.status) { 37 | case "wait": 38 | let left = Math.round((state.reconnect_time - Date.now())/1000); 39 | if(left < 1) { 40 | status_el.textContent = "Reconnecting..." 41 | } else { 42 | status_el.textContent = "Reconnecting in " + left 43 | } 44 | break; 45 | default: 46 | status_el.textContent = state.status; 47 | break; 48 | } 49 | } 50 | 51 | function add_message(msg) { 52 | log('text', "[" + msg.author + "] " + msg.text) 53 | } 54 | 55 | function log(type, message) { 56 | let red = document.createElement('div'); 57 | red.className = type; 58 | red.appendChild(document.createTextNode(message)); 59 | mb.insertBefore(red, mb.childNodes[0]); 60 | } 61 | 62 | -------------------------------------------------------------------------------- /examples/message-board2/messageboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/examples/message-board2/messageboard/__init__.py -------------------------------------------------------------------------------- /examples/message-board2/messageboard/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | from .main import main 3 | main() 4 | -------------------------------------------------------------------------------- /examples/message-board2/messageboard/convention.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | from functools import wraps 5 | 6 | from aiohttp import web 7 | 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class User(object): 13 | 14 | def __init__(self, user_id): 15 | self.user_id = user_id 16 | # this is a hack to get rid of DB 17 | self.username = user_id.replace('_', ' ').title() 18 | 19 | def __repr__(self): 20 | return "".format(self.user_id) 21 | 22 | 23 | class Connection(object): 24 | 25 | def __init__(self, connection_id): 26 | self.connection_id = connection_id 27 | 28 | def __repr__(self): 29 | return "".format(self.connection_id) 30 | 31 | 32 | class Request(object): 33 | 34 | def __init__(self, auth, app, *, 35 | request_id=None, connection_id, **_unused): 36 | self.request_id = request_id 37 | self.connection = Connection(connection_id) 38 | self.app = app 39 | if auth: 40 | kind, value = auth.split(' ') 41 | assert kind == 'Tangle' 42 | auth = json.loads( 43 | base64.b64decode(value.encode('ascii')).decode('utf-8')) 44 | self.user = User(**auth) 45 | 46 | def __repr__(self): 47 | return "".format( 48 | getattr(self, 'user', self.connection)) 49 | 50 | 51 | 52 | def swindon_convention(f): 53 | @wraps(f) 54 | async def swindon_call_method(request): 55 | req = None 56 | try: 57 | metadata, args, kwargs = await request.json() 58 | req = Request(request.headers.get("Authorization"), 59 | request.app, **metadata) 60 | result = await f(req, *args, **kwargs) 61 | return web.json_response(result) 62 | except Exception as e: 63 | log.exception("Error for %r", req or request, exc_info=e) 64 | raise 65 | return swindon_call_method 66 | 67 | -------------------------------------------------------------------------------- /examples/message-board2/messageboard/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import argparse 4 | from aiohttp import web 5 | from censusname import generate as make_name 6 | 7 | from .convention import swindon_convention 8 | from .swindon import connect 9 | 10 | 11 | def main(): 12 | ap = argparse.ArgumentParser() 13 | ap.add_argument('--port', default=8082, help="Listen port") 14 | ap.add_argument('--swindon-port', default=8081, 15 | help="Connect to swindon at port") 16 | options = ap.parse_args() 17 | 18 | logging.basicConfig(level=logging.DEBUG) 19 | app = web.Application() 20 | app['swindon'] = connect(('localhost', options.swindon_port)) 21 | app.router.add_route("POST", "/tangle/authorize_connection", auth) 22 | app.router.add_route("POST", "/message", message) 23 | 24 | 25 | if os.environ.get("LISTEN_FDS") == '1': 26 | # Systemd socket activation protocol 27 | import socket 28 | sock = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) 29 | web.run_app(app, sock=sock) 30 | else: 31 | web.run_app(app, port=options.port) 32 | 33 | 34 | @swindon_convention 35 | async def auth(req, http_authorization, http_cookie, url_querystring): 36 | name = make_name() 37 | id = name.lower().replace(' ', '_') 38 | await req.app['swindon'].subscribe(req.connection, 'message-board') 39 | return { 40 | 'user_id': id, 41 | 'username': name, 42 | } 43 | 44 | 45 | @swindon_convention 46 | async def message(req, text): 47 | await req.app['swindon'].publish('message-board', { 48 | 'author': req.user.username, 49 | 'text': text, 50 | }) 51 | return True 52 | -------------------------------------------------------------------------------- /examples/message-board2/messageboard/swindon.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import aiohttp 4 | 5 | TOPIC_RE = re.compile("^[a-zA-Z0-9_-].") 6 | 7 | 8 | class Swindon(object): 9 | 10 | def __init__(self, addr): 11 | self.addr = addr 12 | self.prefix = 'http://{}:{}/v1/'.format(*self.addr) 13 | self.session = aiohttp.ClientSession() 14 | 15 | async def subscribe(self, conn, topic): 16 | assert TOPIC_RE.match(topic) 17 | async with self.session.put(self.prefix + 18 | 'connection/{}/subscriptions/{}'.format( 19 | conn.connection_id, 20 | topic), 21 | data='') as req: 22 | await req.read() 23 | 24 | 25 | async def publish(self, topic, data): 26 | assert TOPIC_RE.match(topic) 27 | async with self.session.post(self.prefix + 'publish/' + topic, 28 | data=json.dumps(data)) as req: 29 | await req.read() 30 | 31 | 32 | def connect(addr): 33 | return Swindon(addr) 34 | 35 | -------------------------------------------------------------------------------- /examples/message-board2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "message-board2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "babel-loader": "^7.1.1", 7 | "babel-core": "^6.25.0", 8 | "babel-preset-es2015": "^6.24.1", 9 | "webpack": "^3.1.0" 10 | }, 11 | "dependencies": { 12 | "regenerator-runtime": "^0.10.5", 13 | "swindon": "^0.3.2" 14 | }, 15 | "scripts": { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/message-board2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Hello, and welcome to the cranky messageboard!

8 |

Status:

9 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/message-board2/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==2.2.3 2 | censusname==0.2.2 3 | -------------------------------------------------------------------------------- /examples/message-board2/swindon.yaml: -------------------------------------------------------------------------------- 1 | 2 | listen: 3 | - 127.0.0.1:8080 4 | 5 | debug-routing: true 6 | 7 | routing: 8 | localhost/empty.gif: empty-gif 9 | localhost/js: public 10 | localhost/css: public 11 | localhost: chat 12 | localhost/~~swindon-status: status 13 | 14 | handlers: 15 | 16 | chat: !SwindonLattice 17 | 18 | session-pool: chat 19 | http-route: html 20 | compatibility: v0.6.2 21 | 22 | message-handlers: 23 | "*": chat 24 | 25 | empty-gif: !EmptyGif 26 | status: !SelfStatus 27 | 28 | public: !Static 29 | mode: relative_to_domain_root 30 | path: ./public 31 | text-charset: utf-8 32 | 33 | html: !SingleFile 34 | path: ./public/index.html 35 | content-type: "text/html; charset=utf-8" 36 | 37 | 38 | session-pools: 39 | chat: 40 | listen: [127.0.0.1:8081] 41 | inactivity-handlers: [chat] 42 | 43 | 44 | http-destinations: 45 | chat: 46 | override-host-header: swindon.internal 47 | addresses: 48 | - 127.0.0.1:8082 49 | -------------------------------------------------------------------------------- /examples/message-board2/swindon1.yaml: -------------------------------------------------------------------------------- 1 | listen: 2 | - 127.0.0.2:8080 3 | 4 | debug-routing: true 5 | 6 | routing: 7 | "*": chat 8 | "*/css": public 9 | "*/empty.gif": empty-gif 10 | "*/js": public 11 | "*/~~swindon-status": status 12 | 13 | handlers: 14 | 15 | chat: !SwindonLattice 16 | 17 | session-pool: chat 18 | http-route: html 19 | compatibility: v0.6.2 20 | 21 | message-handlers: 22 | "*": chat 23 | 24 | empty-gif: !EmptyGif 25 | status: !SelfStatus 26 | 27 | public: !Static 28 | mode: relative_to_domain_root 29 | path: ./public 30 | text-charset: utf-8 31 | 32 | html: !SingleFile 33 | path: ./public/index.html 34 | content-type: "text/html; charset=utf-8" 35 | 36 | 37 | session-pools: 38 | chat: 39 | listen: [127.0.0.1:8081] 40 | inactivity-handlers: [chat] 41 | 42 | 43 | http-destinations: 44 | chat: 45 | override-host-header: swindon.internal 46 | addresses: 47 | - 127.0.0.1:8082 48 | 49 | replication: 50 | listen: 51 | - 127.0.0.2:7878 52 | peers: 53 | - 127.0.0.3:7878 54 | -------------------------------------------------------------------------------- /examples/message-board2/swindon2.yaml: -------------------------------------------------------------------------------- 1 | listen: 2 | - 127.0.0.3:8080 3 | 4 | debug-routing: true 5 | 6 | routing: 7 | "*": chat 8 | "*/css": public 9 | "*/empty.gif": empty-gif 10 | "*/js": public 11 | "*/~~swindon-status": status 12 | 13 | handlers: 14 | 15 | chat: !SwindonLattice 16 | 17 | session-pool: chat 18 | http-route: html 19 | compatibility: v0.6.2 20 | 21 | message-handlers: 22 | "*": chat 23 | 24 | empty-gif: !EmptyGif 25 | status: !SelfStatus 26 | 27 | public: !Static 28 | mode: relative_to_domain_root 29 | path: ./public 30 | text-charset: utf-8 31 | 32 | html: !SingleFile 33 | path: ./public/index.html 34 | content-type: "text/html; charset=utf-8" 35 | 36 | 37 | session-pools: 38 | chat: 39 | listen: [127.0.0.1:8091] 40 | inactivity-handlers: [chat] 41 | 42 | 43 | http-destinations: 44 | chat: 45 | override-host-header: swindon.internal 46 | addresses: 47 | - 127.0.0.1:8082 48 | 49 | replication: 50 | listen: 51 | - 127.0.0.3:7878 52 | peers: 53 | - 127.0.0.2:7878 54 | -------------------------------------------------------------------------------- /examples/message-board2/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './messageboard.js', 5 | output: { 6 | filename: 'bundle.js', 7 | path: path.resolve(__dirname, 'public/js') 8 | }, 9 | module: { 10 | rules: [{ 11 | loader: 'babel-loader', 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | }], 15 | }, 16 | resolve: { 17 | modules: process.env.NODE_PATH.split(':'), 18 | }, 19 | resolveLoader: { 20 | modules: process.env.NODE_PATH.split(':'), 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /examples/multi-user-chat/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /examples/multi-user-chat/README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Multi-user Chat Example 3 | ======================= 4 | 5 | This is a minimal multi-user chat example using: 6 | 7 | * `python3 `_ 8 | * `sanic `_ 9 | * `aiohttp.client `_ 10 | * `create-react-app `_ 11 | 12 | -------------------------------------------------------------------------------- /examples/multi-user-chat/muc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/examples/multi-user-chat/muc/__init__.py -------------------------------------------------------------------------------- /examples/multi-user-chat/muc/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | from .main import main 3 | main() 4 | -------------------------------------------------------------------------------- /examples/multi-user-chat/muc/chat.py: -------------------------------------------------------------------------------- 1 | from collections import deque, defaultdict 2 | 3 | 4 | USERS = {} 5 | ROOMS = {} 6 | 7 | 8 | class Room(object): 9 | 10 | def __init__(self, name): 11 | self.name = name 12 | self.counter = 0 13 | self.messages = deque(maxlen=64) 14 | 15 | def add(self, author, text): 16 | self.counter += 1 17 | data = {'id': self.counter, 'text': text} 18 | self.messages.append(data) 19 | return data 20 | 21 | def get_history(self, first_id): 22 | return [m for m in self.messages if m['id'] > first_id] 23 | 24 | 25 | class User(object): 26 | 27 | def __init__(self, uid, **props): 28 | self.uid = uid 29 | self.__dict__.update(props) 30 | self.rooms_last_seen = defaultdict(int) 31 | 32 | def update(self, meta): 33 | self.__dict__.update(meta) 34 | 35 | def initial_lattice(self): 36 | shared = {} 37 | mine = {} 38 | for k, n in self.rooms_last_seen.items(): 39 | shared[k] = { 40 | 'last_message_counter': ROOMS[k].counter} 41 | mine[k] = {'last_seen_counter': n} 42 | return { 43 | 'shared': shared, 44 | 'private': { 45 | self.uid: mine, 46 | } 47 | } 48 | 49 | def add_room(self, room): 50 | if room in self.rooms_last_seen: 51 | return {} 52 | if not room in ROOMS: 53 | r = ROOMS[room] = Room(room) 54 | else: 55 | r = ROOMS[room] 56 | self.rooms_last_seen[room] = r.counter 57 | return { 58 | 'shared': { room: {'last_message_counter': r.counter} }, 59 | 'private': { 60 | self.uid: { 61 | room: {'last_seen_counter': self.rooms_last_seen[room]}, 62 | }, 63 | } 64 | } 65 | 66 | 67 | def ensure_user(uid, **meta): 68 | if uid not in USERS: 69 | user = USERS[uid] = User(uid, **meta) 70 | else: 71 | user = USERS[uid] 72 | user.update(meta) 73 | return user 74 | 75 | 76 | def get_user(uid): 77 | return USERS.get(uid) 78 | 79 | def get_room(room): 80 | return ROOMS.get(room) 81 | -------------------------------------------------------------------------------- /examples/multi-user-chat/muc/convention.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | from functools import wraps 5 | 6 | from sanic.response import json as response 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class Connection(object): 12 | 13 | def __init__(self, connection_id): 14 | self.connection_id = connection_id 15 | 16 | def __repr__(self): 17 | return "".format(self.connection_id) 18 | 19 | 20 | class Request(object): 21 | 22 | def __init__(self, auth, *, request_id=None, connection_id, **_unused): 23 | self.request_id = request_id 24 | self.connection = Connection(connection_id) 25 | if auth: 26 | kind, value = auth.split(' ') 27 | assert kind == 'Tangle' 28 | auth = json.loads( 29 | base64.b64decode(value.encode('ascii')).decode('utf-8')) 30 | self.user_id = auth['user_id'] 31 | 32 | def __repr__(self): 33 | return "".format( 34 | getattr(self, 'user', self.connection)) 35 | 36 | 37 | 38 | def swindon_convention(f): 39 | @wraps(f) 40 | async def swindon_call_method(request): 41 | req = None 42 | try: 43 | metadata, args, kwargs = request.json 44 | req = Request(request.headers.get("Authorization"), **metadata) 45 | result = await f(req, *args, **kwargs) 46 | return response(result) 47 | except Exception as e: 48 | log.exception("Error for %r", req or request, exc_info=e) 49 | raise 50 | return swindon_call_method 51 | 52 | -------------------------------------------------------------------------------- /examples/multi-user-chat/muc/swindon.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import aiohttp 4 | 5 | 6 | TOPIC_RE = re.compile("^[a-zA-Z0-9_-].") 7 | 8 | 9 | class Swindon(object): 10 | 11 | def __init__(self, addr, loop): 12 | self.addr = addr 13 | self.prefix = 'http://{}:{}/v1/'.format(*self.addr) 14 | self.session = aiohttp.ClientSession(loop=loop) 15 | 16 | async def attach(self, conn, namespace, initial_data): 17 | assert TOPIC_RE.match(namespace) 18 | async with self.session.put(self.prefix + 19 | 'connection/{}/lattices/{}'.format( 20 | conn.connection_id, 21 | namespace.replace('.', '/')), 22 | data=json.dumps(initial_data)) as req: 23 | assert req.status == 204, req.status 24 | await req.read() 25 | 26 | async def lattice(self, namespace, delta): 27 | assert TOPIC_RE.match(namespace) 28 | async with self.session.post(self.prefix + 29 | 'lattice/{}'.format(namespace), 30 | data=json.dumps(delta)) as req: 31 | assert req.status == 204, req.status 32 | await req.read() 33 | 34 | async def subscribe(self, conn, topic): 35 | assert TOPIC_RE.match(topic) 36 | async with self.session.put(self.prefix + 37 | 'connection/{}/subscriptions/{}'.format( 38 | conn.connection_id, 39 | topic.replace('.', '/')), 40 | data='') as req: 41 | assert req.status == 204, req.status 42 | await req.read() 43 | 44 | async def unsubscribe(self, conn, topic): 45 | assert TOPIC_RE.match(topic) 46 | async with self.session.request('DELETE', self.prefix + 47 | 'connection/{}/subscriptions/{}'.format( 48 | conn.connection_id, 49 | topic.replace('.', '/')), 50 | ) as req: 51 | assert req.status == 204, req.status 52 | await req.read() 53 | 54 | async def publish(self, topic, data): 55 | assert TOPIC_RE.match(topic) 56 | async with self.session.post( 57 | self.prefix + 'publish/' + topic.replace('.', '/'), 58 | data=json.dumps(data)) as req: 59 | assert req.status == 204, req.status 60 | await req.read() 61 | 62 | 63 | def connect(addr, loop): 64 | return Swindon(addr, loop=loop) 65 | 66 | -------------------------------------------------------------------------------- /examples/multi-user-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tmp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.7.0" 7 | }, 8 | "dependencies": { 9 | "react": "^15.4.0", 10 | "react-dom": "^15.4.0", 11 | "react-router": "3.0.0", 12 | "cookie": "0.3.1", 13 | "classnames": "2.2.5" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/multi-user-chat/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/examples/multi-user-chat/public/favicon.ico -------------------------------------------------------------------------------- /examples/multi-user-chat/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React App 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/multi-user-chat/requirements.txt: -------------------------------------------------------------------------------- 1 | sanic==0.1.7 2 | aiohttp==1.1.5 3 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/components/Chat.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | import classnames from 'classnames' 4 | import * as websocket from '../websocket' 5 | 6 | import './chat.css' 7 | 8 | export default class Chat extends Component { 9 | 10 | render() { 11 | const { className, title, children } = this.props; 12 | return ( 13 |
14 |
15 |
    16 | { 17 | websocket.room_list.map(room => ( 18 |
  • 19 | { room.name } 22 | { room.unseen } 24 |
  • 25 | )) 26 | } 27 |
28 |
29 |
30 |
31 | { title ||

No room selected

} 32 | [{ websocket.state }] 33 |
34 |
35 | { children } 36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classnames from 'classnames'; 3 | import { Link } from 'react-router'; 4 | import render from '../render'; 5 | import {get_login} from '../login' 6 | 7 | import "./login.css" 8 | 9 | export default class Login extends Component { 10 | 11 | setLogin(event) { 12 | document.cookie = "swindon_muc_login=" + event.target.value; 13 | render() 14 | } 15 | render() { 16 | const { className } = this.props; 17 | const login = get_login(); 18 | return ( 19 |
20 |

21 | Sign-in 22 |

23 | 26 | {login && 27 | 28 | } 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/components/Room.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import classnames from 'classnames' 3 | import * as websocket from '../websocket' 4 | 5 | export class Title extends Component { 6 | render() { 7 | const { params: {roomName} } = this.props; 8 | return

{ roomName }

9 | } 10 | } 11 | 12 | export class Main extends Component { 13 | render() { 14 | const { className } = this.props; 15 | return ( 16 |
17 |
18 |
    19 | { (websocket.current_room_messages || []).map(m => ( 20 |
  • 21 | { m.author } 22 | { m.text } 23 |
  • 24 | )) 25 | } 26 |
27 |
28 |
29 | this.setState({text: e.target.value}) } 32 | onKeyDown={ e => { 33 | if(e.which === 13 && this.state && this.state.text) { 34 | websocket.send_message(this.state.text) 35 | this.setState({text: ''}) 36 | } 37 | }}/> 38 |
39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/components/SelectRoom.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, browserHistory } from 'react-router'; 3 | import classnames from 'classnames'; 4 | 5 | 6 | export class Main extends Component { 7 | render() { 8 | const { className } = this.props; 9 | return
10 |

11 | 12 | Enter room name in address bar or here: 13 |   14 | 15 | 16 | this.setState({roomName: e.target.value}) } 18 | onKeyDown={ e => { 19 | if(e.which === 13 && this.state && this.state.roomName) { 20 | browserHistory.push('/' + this.state.roomName) 21 | } 22 | }}/> 23 | {this.state && this.state.roomName && 24 | 25 | 26 | } 27 | 28 | /kittens 29 | /cars 30 | 31 |

32 |

33 | 34 | Or select room from your room list (if you've visited before) 35 |

36 |
37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/components/chat.css: -------------------------------------------------------------------------------- 1 | .Chat { 2 | display: flex; 3 | flex-direction: row; 4 | position: absolute; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | .title-block { 9 | border-bottom: solid gray 1px; 10 | padding-bottom: 8px; 11 | display: flex; 12 | flex-direction: row; 13 | min-height: 4em; 14 | } 15 | .title-block > h1 { 16 | flex-grow: 1; 17 | margin-left: 32px; 18 | } 19 | .room-title:before { 20 | content: "«"; 21 | } 22 | .room-title:after { 23 | content: "»"; 24 | } 25 | .chat-body { 26 | flex-grow: 1; 27 | display: flex; 28 | flex-direction: column; 29 | } 30 | .connection-status { 31 | padding: 12px; 32 | } 33 | 34 | .room-list { 35 | width: 18%; 36 | min-width: 250px; 37 | border-right: dashed gray 1px; 38 | margin-right: 8px 39 | } 40 | .big-arrow { 41 | display: inline-block; 42 | font-size: 500%; 43 | width: 1em; 44 | text-align: center; 45 | } 46 | 47 | .select-room-input-block { 48 | display: inline-flex; 49 | width: 15em; 50 | flex-wrap: wrap; 51 | } 52 | .select-room-input-box { 53 | width: 100%; 54 | } 55 | .select-room-input-block > a { 56 | padding: 3px 57 | } 58 | .room-list--room { 59 | display: flex; 60 | } 61 | .room-list--room-name { 62 | flex-grow: 1; 63 | } 64 | .room-list--unread { 65 | padding: 2px 8px; 66 | } 67 | 68 | .message-field { 69 | flex-grow: 1; 70 | display: flex; 71 | } 72 | .Room { 73 | display: flex; 74 | flex-direction: column; 75 | flex-grow: 1; 76 | } 77 | .message-box { 78 | flex-grow: 1; 79 | overflow-y: auto; 80 | width: 100%; 81 | } 82 | .messages { 83 | } 84 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/components/login.css: -------------------------------------------------------------------------------- 1 | .Login { 2 | padding: 24px 64px; 3 | } 4 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/index.js: -------------------------------------------------------------------------------- 1 | import render from './render'; 2 | 3 | import './index.css'; 4 | 5 | render(); 6 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/login.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | 3 | 4 | export function get_login() { 5 | let {swindon_muc_login} = cookie.parse(document.cookie); 6 | return swindon_muc_login 7 | } 8 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { browserHistory } from 'react-router'; 4 | 5 | import Routes from './routes'; 6 | 7 | 8 | export default function render() { 9 | ReactDOM.render( 10 | , 11 | document.getElementById('root') 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/multi-user-chat/src/routes.js: -------------------------------------------------------------------------------- 1 | // src/routes.js 2 | import React from 'react' 3 | import { Router, Route, IndexRoute } from 'react-router' 4 | 5 | import * as websocket from './websocket' 6 | import Login from './components/Login' 7 | import Chat from './components/Chat' 8 | import * as room from './components/Room' 9 | import * as select from './components/SelectRoom' 10 | import {get_login} from './login' 11 | 12 | 13 | function check_login(route, replace) { 14 | let {location: {pathname}} = route; 15 | if(pathname !== '/login' && !get_login()) { 16 | replace('/login') 17 | } 18 | } 19 | 20 | const Routes = ({history, ...props}) => ( 21 | 22 | 23 | 24 | 26 | 27 | 31 | 32 | 33 | 34 | ) 35 | 36 | export default Routes 37 | -------------------------------------------------------------------------------- /examples/multi-user-chat/swindon.yaml: -------------------------------------------------------------------------------- 1 | 2 | listen: 3 | - 127.0.0.1:8080 4 | 5 | debug-routing: true 6 | 7 | routing: 8 | localhost/empty.gif: empty-gif 9 | localhost/favicon.ico: public 10 | localhost: chat 11 | localhost/sockjs-node: socksjs 12 | localhost/~~swindon-status: status 13 | 14 | 15 | handlers: 16 | 17 | chat: !SwindonLattice 18 | 19 | session-pool: chat 20 | http-route: html 21 | compatibility: v0.6.2 22 | 23 | message-handlers: 24 | "*": chat 25 | 26 | empty-gif: !EmptyGif 27 | status: !SelfStatus 28 | 29 | public: !Static 30 | mode: relative_to_domain_root 31 | path: ./public 32 | text-charset: utf-8 33 | 34 | html: !Proxy 35 | destination: webpack 36 | 37 | # The way socksjs does websocket emulation: it creates a response with 38 | # chunked encoding. While we might be able to process it well, it occupies 39 | # connection and the request that is pipelined after this one hangs 40 | # indefinitely. So we need separate connection pool for such connections 41 | socksjs: !Proxy 42 | destination: socksjs-emu 43 | 44 | 45 | session-pools: 46 | chat: 47 | listen: [127.0.0.1:8081] 48 | inactivity-handlers: [chat] 49 | 50 | 51 | http-destinations: 52 | chat: 53 | override-host-header: swindon.internal 54 | addresses: 55 | - 127.0.0.1:8082 56 | 57 | webpack: 58 | addresses: 59 | - 127.0.0.1:3000 60 | 61 | socksjs-emu: 62 | addresses: 63 | - 127.0.0.1:3000 64 | backend-connections-per-ip-port: 100 65 | in-flight-requests-per-backend-connection: 1 66 | queue-size-for-503: 1 67 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Multi-user Chat Example 3 | ======================= 4 | 5 | This is an updated multi-user chat example which uses swindon-js library 6 | instead of using websocket directly. Tools used: 7 | 8 | * `python3 `_ 9 | * `sanic `_ 10 | * `aiohttp.client `_ 11 | * `swindon-js `_ 12 | * `create-react-app `_ 13 | 14 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/muc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/examples/multi-user-chat2/muc/__init__.py -------------------------------------------------------------------------------- /examples/multi-user-chat2/muc/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | from .main import main 3 | main() 4 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/muc/chat.py: -------------------------------------------------------------------------------- 1 | from collections import deque, defaultdict 2 | 3 | 4 | USERS = {} 5 | ROOMS = {} 6 | 7 | 8 | class Room(object): 9 | 10 | def __init__(self, name): 11 | self.name = name 12 | self.counter = 0 13 | self.messages = deque(maxlen=64) 14 | 15 | def add(self, author, text): 16 | self.counter += 1 17 | data = {'id': self.counter, 'text': text} 18 | self.messages.append(data) 19 | return data 20 | 21 | def get_history(self, first_id): 22 | return [m for m in self.messages if m['id'] > first_id] 23 | 24 | 25 | class User(object): 26 | 27 | def __init__(self, uid, **props): 28 | self.uid = uid 29 | self.__dict__.update(props) 30 | self.rooms_last_seen = defaultdict(int) 31 | 32 | def update(self, meta): 33 | self.__dict__.update(meta) 34 | 35 | def initial_lattice(self): 36 | shared = {} 37 | mine = {} 38 | for k, n in self.rooms_last_seen.items(): 39 | shared[k] = { 40 | 'last_message_counter': ROOMS[k].counter} 41 | mine[k] = {'last_seen_counter': n} 42 | return { 43 | 'shared': shared, 44 | 'private': { 45 | self.uid: mine, 46 | } 47 | } 48 | 49 | def add_room(self, room): 50 | if room in self.rooms_last_seen: 51 | return {} 52 | if not room in ROOMS: 53 | r = ROOMS[room] = Room(room) 54 | else: 55 | r = ROOMS[room] 56 | self.rooms_last_seen[room] = r.counter 57 | return { 58 | 'shared': { room: {'last_message_counter': r.counter} }, 59 | 'private': { 60 | self.uid: { 61 | room: {'last_seen_counter': self.rooms_last_seen[room]}, 62 | }, 63 | } 64 | } 65 | 66 | 67 | def ensure_user(uid, **meta): 68 | if uid not in USERS: 69 | user = USERS[uid] = User(uid, **meta) 70 | else: 71 | user = USERS[uid] 72 | user.update(meta) 73 | return user 74 | 75 | 76 | def get_user(uid): 77 | return USERS.get(uid) 78 | 79 | def get_room(room): 80 | return ROOMS.get(room) 81 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/muc/convention.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | from functools import wraps 5 | 6 | from sanic.response import json as response 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class Connection(object): 12 | 13 | def __init__(self, connection_id): 14 | self.connection_id = connection_id 15 | 16 | def __repr__(self): 17 | return "".format(self.connection_id) 18 | 19 | 20 | class Request(object): 21 | 22 | def __init__(self, auth, *, request_id=None, connection_id, **_unused): 23 | self.request_id = request_id 24 | self.connection = Connection(connection_id) 25 | if auth: 26 | kind, value = auth.split(' ') 27 | assert kind == 'Tangle' 28 | auth = json.loads( 29 | base64.b64decode(value.encode('ascii')).decode('utf-8')) 30 | self.user_id = auth['user_id'] 31 | 32 | def __repr__(self): 33 | return "".format( 34 | getattr(self, 'user', self.connection)) 35 | 36 | 37 | 38 | def swindon_convention(f): 39 | @wraps(f) 40 | async def swindon_call_method(request): 41 | req = None 42 | try: 43 | metadata, args, kwargs = request.json 44 | req = Request(request.headers.get("Authorization"), **metadata) 45 | result = await f(req, *args, **kwargs) 46 | return response(result) 47 | except Exception as e: 48 | log.exception("Error for %r", req or request, exc_info=e) 49 | raise 50 | return swindon_call_method 51 | 52 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/muc/swindon.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import aiohttp 4 | 5 | 6 | TOPIC_RE = re.compile("^[a-zA-Z0-9_-].") 7 | 8 | 9 | class Swindon(object): 10 | 11 | def __init__(self, addr, loop): 12 | self.addr = addr 13 | self.prefix = 'http://{}:{}/v1/'.format(*self.addr) 14 | self.session = aiohttp.ClientSession(loop=loop) 15 | 16 | async def attach(self, conn, namespace, initial_data): 17 | assert TOPIC_RE.match(namespace) 18 | async with self.session.put(self.prefix + 19 | 'connection/{}/lattices/{}'.format( 20 | conn.connection_id, 21 | namespace.replace('.', '/')), 22 | data=json.dumps(initial_data)) as req: 23 | assert req.status == 204, req.status 24 | await req.read() 25 | 26 | async def lattice(self, namespace, delta): 27 | assert TOPIC_RE.match(namespace) 28 | async with self.session.post(self.prefix + 29 | 'lattice/{}'.format(namespace), 30 | data=json.dumps(delta)) as req: 31 | assert req.status == 204, req.status 32 | await req.read() 33 | 34 | async def subscribe(self, conn, topic): 35 | assert TOPIC_RE.match(topic) 36 | async with self.session.put(self.prefix + 37 | 'connection/{}/subscriptions/{}'.format( 38 | conn.connection_id, 39 | topic.replace('.', '/')), 40 | data='') as req: 41 | assert req.status == 204, req.status 42 | await req.read() 43 | 44 | async def unsubscribe(self, conn, topic): 45 | assert TOPIC_RE.match(topic) 46 | async with self.session.request('DELETE', self.prefix + 47 | 'connection/{}/subscriptions/{}'.format( 48 | conn.connection_id, 49 | topic.replace('.', '/')), 50 | ) as req: 51 | assert req.status == 204, req.status 52 | await req.read() 53 | 54 | async def publish(self, topic, data): 55 | assert TOPIC_RE.match(topic) 56 | async with self.session.post( 57 | self.prefix + 'publish/' + topic.replace('.', '/'), 58 | data=json.dumps(data)) as req: 59 | assert req.status == 204, req.status 60 | await req.read() 61 | 62 | 63 | def connect(addr, loop): 64 | return Swindon(addr, loop=loop) 65 | 66 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tmp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.7.0" 7 | }, 8 | "dependencies": { 9 | "react": "^15.4.0", 10 | "react-dom": "^15.4.0", 11 | "react-router": "3.0.0", 12 | "cookie": "0.3.1", 13 | "swindon": "0.4.0", 14 | "classnames": "2.2.5" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React App 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/requirements.txt: -------------------------------------------------------------------------------- 1 | sanic==0.1.7 2 | aiohttp==1.1.5 3 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/components/Chat.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | import classnames from 'classnames' 4 | import * as server from '../server' 5 | 6 | import './chat.css' 7 | 8 | export default class Chat extends Component { 9 | 10 | render() { 11 | const { className, title, children } = this.props; 12 | return ( 13 |
14 |
15 |
    16 | { 17 | server.room_list.map(room => ( 18 |
  • 19 | { room.name } 22 | { room.unseen } 24 |
  • 25 | )) 26 | } 27 |
28 |
29 |
30 |
31 | { title ||

No room selected

} 32 | [{ server.state }] 33 |
34 |
35 | { children } 36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classnames from 'classnames'; 3 | import { Link } from 'react-router'; 4 | import render from '../render'; 5 | import {get_login} from '../login' 6 | 7 | import "./login.css" 8 | 9 | export default class Login extends Component { 10 | 11 | setLogin(event) { 12 | document.cookie = "swindon_muc_login=" + event.target.value; 13 | render() 14 | } 15 | render() { 16 | const { className } = this.props; 17 | const login = get_login(); 18 | return ( 19 |
20 |

21 | Sign-in 22 |

23 | 26 | {login && 27 | 28 | } 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/components/Room.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import classnames from 'classnames' 3 | import * as server from '../server' 4 | 5 | export class Title extends Component { 6 | render() { 7 | const { params: {roomName} } = this.props; 8 | return

{ roomName }

9 | } 10 | } 11 | 12 | export class Main extends Component { 13 | render() { 14 | const { className } = this.props; 15 | return ( 16 |
17 |
18 |
    19 | { (server.current_room_messages || []).map(m => ( 20 |
  • 21 | { m.author } 22 | { m.text } 23 |
  • 24 | )) 25 | } 26 |
27 |
28 |
29 | this.setState({text: e.target.value}) } 32 | onKeyDown={ e => { 33 | if(e.which === 13 && this.state && this.state.text) { 34 | server.send_message(this.state.text) 35 | this.setState({text: ''}) 36 | } 37 | }}/> 38 |
39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/components/SelectRoom.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, browserHistory } from 'react-router'; 3 | import classnames from 'classnames'; 4 | 5 | 6 | export class Main extends Component { 7 | render() { 8 | const { className } = this.props; 9 | return
10 |

11 | 12 | Enter room name in address bar or here: 13 |   14 | 15 | 16 | this.setState({roomName: e.target.value}) } 18 | onKeyDown={ e => { 19 | if(e.which === 13 && this.state && this.state.roomName) { 20 | browserHistory.push('/' + this.state.roomName) 21 | } 22 | }}/> 23 | {this.state && this.state.roomName && 24 | 25 | 26 | } 27 | 28 | /kittens 29 | /cars 30 | 31 |

32 |

33 | 34 | Or select room from your room list (if you've visited before) 35 |

36 |
37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/components/chat.css: -------------------------------------------------------------------------------- 1 | .Chat { 2 | display: flex; 3 | flex-direction: row; 4 | position: absolute; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | .title-block { 9 | border-bottom: solid gray 1px; 10 | padding-bottom: 8px; 11 | display: flex; 12 | flex-direction: row; 13 | min-height: 4em; 14 | } 15 | .title-block > h1 { 16 | flex-grow: 1; 17 | margin-left: 32px; 18 | } 19 | .room-title:before { 20 | content: "«"; 21 | } 22 | .room-title:after { 23 | content: "»"; 24 | } 25 | .chat-body { 26 | flex-grow: 1; 27 | display: flex; 28 | flex-direction: column; 29 | } 30 | .connection-status { 31 | padding: 12px; 32 | } 33 | 34 | .room-list { 35 | width: 18%; 36 | min-width: 250px; 37 | border-right: dashed gray 1px; 38 | margin-right: 8px 39 | } 40 | .big-arrow { 41 | display: inline-block; 42 | font-size: 500%; 43 | width: 1em; 44 | text-align: center; 45 | } 46 | 47 | .select-room-input-block { 48 | display: inline-flex; 49 | width: 15em; 50 | flex-wrap: wrap; 51 | } 52 | .select-room-input-box { 53 | width: 100%; 54 | } 55 | .select-room-input-block > a { 56 | padding: 3px 57 | } 58 | .room-list--room { 59 | display: flex; 60 | } 61 | .room-list--room-name { 62 | flex-grow: 1; 63 | } 64 | .room-list--unread { 65 | padding: 2px 8px; 66 | } 67 | 68 | .message-field { 69 | flex-grow: 1; 70 | display: flex; 71 | } 72 | .Room { 73 | display: flex; 74 | flex-direction: column; 75 | flex-grow: 1; 76 | } 77 | .message-box { 78 | flex-grow: 1; 79 | overflow-y: auto; 80 | width: 100%; 81 | } 82 | .messages { 83 | } 84 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/components/login.css: -------------------------------------------------------------------------------- 1 | .Login { 2 | padding: 24px 64px; 3 | } 4 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/index.js: -------------------------------------------------------------------------------- 1 | import render from './render'; 2 | 3 | import './index.css'; 4 | 5 | render(); 6 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/login.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | 3 | 4 | export function get_login() { 5 | let {swindon_muc_login} = cookie.parse(document.cookie); 6 | return swindon_muc_login 7 | } 8 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { browserHistory } from 'react-router'; 4 | 5 | import Routes from './routes'; 6 | 7 | 8 | export default function render() { 9 | ReactDOM.render( 10 | , 11 | document.getElementById('root') 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/src/routes.js: -------------------------------------------------------------------------------- 1 | // src/routes.js 2 | import React from 'react' 3 | import { Router, Route, IndexRoute } from 'react-router' 4 | 5 | import * as server from './server' 6 | import Login from './components/Login' 7 | import Chat from './components/Chat' 8 | import * as room from './components/Room' 9 | import * as select from './components/SelectRoom' 10 | import {get_login} from './login' 11 | 12 | 13 | function check_login(route, replace) { 14 | let {location: {pathname}} = route; 15 | if(pathname !== '/login' && !get_login()) { 16 | replace('/login') 17 | } 18 | } 19 | 20 | const Routes = ({history, ...props}) => ( 21 | 22 | 23 | 24 | 26 | 27 | 31 | 32 | 33 | 34 | ) 35 | 36 | export default Routes 37 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/swindon.yaml: -------------------------------------------------------------------------------- 1 | 2 | listen: 3 | - 127.0.0.1:8080 4 | 5 | debug-routing: true 6 | 7 | routing: 8 | localhost/empty.gif: empty-gif 9 | localhost/favicon.ico: public 10 | localhost: chat 11 | localhost/sockjs-node: socksjs 12 | localhost/~~swindon-status: status 13 | 14 | 15 | handlers: 16 | 17 | chat: !SwindonLattice 18 | 19 | session-pool: chat 20 | http-route: html 21 | compatibility: v0.6.2 22 | 23 | message-handlers: 24 | "*": chat 25 | 26 | empty-gif: !EmptyGif 27 | status: !SelfStatus 28 | 29 | public: !Static 30 | mode: relative_to_domain_root 31 | path: ./public 32 | text-charset: utf-8 33 | 34 | html: !Proxy 35 | destination: webpack 36 | 37 | # The way socksjs does websocket emulation: it creates a response with 38 | # chunked encoding. While we might be able to process it well, it occupies 39 | # connection and the request that is pipelined after this one hangs 40 | # indefinitely. So we need separate connection pool for such connections 41 | socksjs: !Proxy 42 | destination: socksjs-emu 43 | 44 | 45 | session-pools: 46 | chat: 47 | listen: [127.0.0.1:8081] 48 | inactivity-handlers: [chat] 49 | 50 | 51 | http-destinations: 52 | chat: 53 | override-host-header: swindon.internal 54 | addresses: 55 | - 127.0.0.1:8082 56 | 57 | webpack: 58 | addresses: 59 | - 127.0.0.1:3000 60 | 61 | socksjs-emu: 62 | addresses: 63 | - 127.0.0.1:3000 64 | backend-connections-per-ip-port: 100 65 | in-flight-requests-per-backend-connection: 1 66 | queue-size-for-503: 1 67 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/swindon1.yaml: -------------------------------------------------------------------------------- 1 | listen: 2 | - 127.0.0.2:8080 3 | 4 | debug-routing: true 5 | 6 | routing: 7 | "*": chat 8 | "*/empty.gif": empty-gif 9 | "*/favicon.ico": public 10 | "*/sockjs-node": socksjs 11 | "*/~~swindon-status": status 12 | 13 | 14 | handlers: 15 | 16 | chat: !SwindonLattice 17 | 18 | session-pool: chat 19 | http-route: html 20 | compatibility: v0.6.2 21 | 22 | message-handlers: 23 | "*": chat 24 | 25 | empty-gif: !EmptyGif 26 | status: !SelfStatus 27 | 28 | public: !Static 29 | mode: relative_to_domain_root 30 | path: ./public 31 | text-charset: utf-8 32 | 33 | html: !Proxy 34 | destination: webpack 35 | 36 | # The way socksjs does websocket emulation: it creates a response with 37 | # chunked encoding. While we might be able to process it well, it occupies 38 | # connection and the request that is pipelined after this one hangs 39 | # indefinitely. So we need separate connection pool for such connections 40 | socksjs: !Proxy 41 | destination: socksjs-emu 42 | 43 | 44 | session-pools: 45 | chat: 46 | listen: [127.0.0.1:8081] 47 | inactivity-handlers: [chat] 48 | 49 | 50 | http-destinations: 51 | chat: 52 | override-host-header: swindon.internal 53 | addresses: 54 | - 127.0.0.1:8082 55 | 56 | webpack: 57 | addresses: 58 | - 127.0.0.1:3000 59 | 60 | socksjs-emu: 61 | addresses: 62 | - 127.0.0.1:3000 63 | backend-connections-per-ip-port: 100 64 | in-flight-requests-per-backend-connection: 1 65 | queue-size-for-503: 1 66 | 67 | replication: 68 | listen: 69 | - 127.0.0.3:7878 70 | peers: 71 | - 127.0.0.2:7878 72 | -------------------------------------------------------------------------------- /examples/multi-user-chat2/swindon2.yaml: -------------------------------------------------------------------------------- 1 | listen: 2 | - 127.0.0.3:8080 3 | 4 | debug-routing: true 5 | 6 | routing: 7 | "*": chat 8 | "*/empty.gif": empty-gif 9 | "*/favicon.ico": public 10 | "*/sockjs-node": socksjs 11 | "*/~~swindon-status": status 12 | 13 | 14 | handlers: 15 | 16 | chat: !SwindonLattice 17 | 18 | session-pool: chat 19 | http-route: html 20 | compatibility: v0.6.2 21 | 22 | message-handlers: 23 | "*": chat 24 | 25 | empty-gif: !EmptyGif 26 | status: !SelfStatus 27 | 28 | public: !Static 29 | mode: relative_to_domain_root 30 | path: ./public 31 | text-charset: utf-8 32 | 33 | html: !Proxy 34 | destination: webpack 35 | 36 | # The way socksjs does websocket emulation: it creates a response with 37 | # chunked encoding. While we might be able to process it well, it occupies 38 | # connection and the request that is pipelined after this one hangs 39 | # indefinitely. So we need separate connection pool for such connections 40 | socksjs: !Proxy 41 | destination: socksjs-emu 42 | 43 | 44 | session-pools: 45 | chat: 46 | listen: [127.0.0.1:8091] 47 | inactivity-handlers: [chat] 48 | 49 | 50 | http-destinations: 51 | chat: 52 | override-host-header: swindon.internal 53 | addresses: 54 | - 127.0.0.1:8082 55 | 56 | webpack: 57 | addresses: 58 | - 127.0.0.1:3000 59 | 60 | socksjs-emu: 61 | addresses: 62 | - 127.0.0.1:3000 63 | backend-connections-per-ip-port: 100 64 | in-flight-requests-per-backend-connection: 1 65 | queue-size-for-503: 1 66 | 67 | replication: 68 | listen: 69 | - 127.0.0.2:7878 70 | peers: 71 | - 127.0.0.3:7878 72 | -------------------------------------------------------------------------------- /examples/presence/.gitignore: -------------------------------------------------------------------------------- 1 | /public/js 2 | -------------------------------------------------------------------------------- /examples/presence/README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Presence Board Example 3 | ====================== 4 | 5 | This is a minimal example of app tracking user presence (i.e. online status). 6 | This is usually a part of a chat or messenger application, but here we show 7 | only that part. 8 | 9 | * `python3 `_ 10 | * `aiohttp.web `_ 11 | * `swindon-js `_ 12 | * `marko `_ 13 | * `webpack `_ 14 | 15 | -------------------------------------------------------------------------------- /examples/presence/auth.marko: -------------------------------------------------------------------------------- 1 | import {history} from 'marko-path-router' 2 | 3 | class { 4 | login() { 5 | let value = this.getEl("name").value 6 | if(value.length > 0) { 7 | document.cookie = "swindon_presence_login=" + value 8 | history.push('/list') 9 | } 10 | } 11 | } 12 | 13 | 16 | -------------------------------------------------------------------------------- /examples/presence/index.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie' 2 | import auth from './auth.marko' 3 | import user_list from './user_list.marko' 4 | import { Router } from 'marko-path-router' 5 | 6 | var {swindon_presence_login} = cookie.parse(document.cookie) 7 | 8 | let render = Router.renderSync({ 9 | initialRoute: swindon_presence_login ? '/list' : '/login', 10 | routes: [ 11 | {path: '/login', component: auth }, 12 | {path: '/list', component: user_list }, 13 | ], 14 | }) 15 | 16 | render.appendTo(document.body) 17 | -------------------------------------------------------------------------------- /examples/presence/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "presence", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "babel-loader": "^7.1.1", 7 | "babel-core": "^6.25.0", 8 | "babel-preset-es2015": "^6.24.1", 9 | "cookie": "0.3.1", 10 | "css-loader": "0.28.7", 11 | "marko": "^4.4.26", 12 | "marko-loader": "^1.3.1", 13 | "marko-path-router": "^0.5.0", 14 | "webpack": "^3.1.0" 15 | }, 16 | "dependencies": { 17 | "regenerator-runtime": "^0.10.5", 18 | "swindon": "git://github.com/swindon-rs/swindon-js" 19 | }, 20 | "scripts": { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/presence/presence/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/examples/presence/presence/__init__.py -------------------------------------------------------------------------------- /examples/presence/presence/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | from .main import main 3 | main() 4 | -------------------------------------------------------------------------------- /examples/presence/presence/convention.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | from functools import wraps 5 | 6 | from aiohttp import web 7 | 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class User(object): 13 | 14 | def __init__(self, user_id): 15 | self.user_id = user_id 16 | # this is a hack to get rid of DB 17 | self.username = user_id.replace('_', ' ').title() 18 | 19 | def __repr__(self): 20 | return "".format(self.user_id) 21 | 22 | 23 | class Connection(object): 24 | 25 | def __init__(self, connection_id): 26 | self.connection_id = connection_id 27 | 28 | def __repr__(self): 29 | return "".format(self.connection_id) 30 | 31 | 32 | class Request(object): 33 | 34 | def __init__(self, auth, app, *, 35 | request_id=None, connection_id, **_unused): 36 | self.request_id = request_id 37 | self.connection = Connection(connection_id) 38 | self.app = app 39 | if auth: 40 | kind, value = auth.split(' ') 41 | assert kind == 'Tangle' 42 | auth = json.loads( 43 | base64.b64decode(value.encode('ascii')).decode('utf-8')) 44 | self.user = User(**auth) 45 | 46 | def __repr__(self): 47 | return "".format( 48 | getattr(self, 'user', self.connection)) 49 | 50 | 51 | 52 | def swindon_convention(f): 53 | @wraps(f) 54 | async def swindon_call_method(request): 55 | req = None 56 | try: 57 | metadata, args, kwargs = await request.json() 58 | req = Request(request.headers.get("Authorization"), 59 | request.app, **metadata) 60 | result = await f(req, *args, **kwargs) 61 | return web.json_response(result) 62 | except Exception as e: 63 | log.exception("Error for %r", req or request, exc_info=e) 64 | raise 65 | return swindon_call_method 66 | 67 | -------------------------------------------------------------------------------- /examples/presence/presence/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import logging 4 | import argparse 5 | from http.cookies import SimpleCookie 6 | from aiohttp import web 7 | from censusname import generate as make_name 8 | 9 | from .convention import swindon_convention 10 | from .swindon import connect 11 | 12 | 13 | NON_ALPHA = re.compile('[^a-z0-9_]') 14 | 15 | 16 | def main(): 17 | ap = argparse.ArgumentParser() 18 | ap.add_argument('--port', default=8082, help="Listen port") 19 | ap.add_argument('--swindon-port', default=8081, 20 | help="Connect to swindon at port") 21 | options = ap.parse_args() 22 | 23 | logging.basicConfig(level=logging.DEBUG) 24 | app = web.Application() 25 | app['swindon'] = connect(('localhost', options.swindon_port)) 26 | app.router.add_route("POST", "/tangle/authorize_connection", auth) 27 | app.router.add_route("POST", "/message", message) 28 | 29 | 30 | if os.environ.get("LISTEN_FDS") == '1': 31 | # Systemd socket activation protocol 32 | import socket 33 | sock = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) 34 | web.run_app(app, sock=sock) 35 | else: 36 | web.run_app(app, port=options.port) 37 | 38 | 39 | @swindon_convention 40 | async def auth(req, http_authorization, http_cookie, url_querystring): 41 | name = SimpleCookie(http_cookie)['swindon_presence_login'].value 42 | uid = NON_ALPHA.sub('_', name.lower()) 43 | req.app['swindon'].all_users.add(uid) 44 | await req.app['swindon'].attach_users(req.connection, 'muc') 45 | return { 46 | 'user_id': uid, 47 | 'username': name, 48 | } 49 | 50 | 51 | @swindon_convention 52 | async def message(req, text): 53 | await req.app['swindon'].publish('message-board', { 54 | 'author': req.user.username, 55 | 'text': text, 56 | }) 57 | return True 58 | -------------------------------------------------------------------------------- /examples/presence/presence/swindon.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import aiohttp 4 | 5 | TOPIC_RE = re.compile("^[a-zA-Z0-9_-].") 6 | 7 | 8 | class Swindon(object): 9 | 10 | def __init__(self, addr): 11 | self.addr = addr 12 | self.prefix = 'http://{}:{}/v1/'.format(*self.addr) 13 | self.all_users = set() 14 | self.session = aiohttp.ClientSession() 15 | 16 | async def attach_users(self, conn, namespace): 17 | assert TOPIC_RE.match(namespace) 18 | async with self.session.put(self.prefix + 19 | 'connection/{}/users'.format(conn.connection_id), 20 | data=json.dumps(list(self.all_users))) as req: 21 | assert req.status == 204, req.status 22 | res = await req.read() 23 | print("RES", res) 24 | 25 | 26 | def connect(addr): 27 | return Swindon(addr) 28 | 29 | -------------------------------------------------------------------------------- /examples/presence/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/presence/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==2.2.3 2 | censusname==0.2.2 3 | -------------------------------------------------------------------------------- /examples/presence/swindon.yaml: -------------------------------------------------------------------------------- 1 | 2 | listen: 3 | - 127.0.0.1:8080 4 | 5 | debug-routing: true 6 | 7 | routing: 8 | localhost/empty.gif: empty-gif 9 | localhost/js: public 10 | localhost/css: public 11 | localhost: chat 12 | localhost/~~swindon-status: status 13 | 14 | handlers: 15 | 16 | chat: !SwindonLattice 17 | 18 | session-pool: chat 19 | http-route: html 20 | compatibility: v0.6.2 21 | 22 | message-handlers: 23 | "*": chat 24 | 25 | empty-gif: !EmptyGif 26 | status: !SelfStatus 27 | 28 | public: !Static 29 | mode: relative_to_domain_root 30 | path: ./public 31 | text-charset: utf-8 32 | 33 | html: !SingleFile 34 | path: ./public/index.html 35 | content-type: "text/html; charset=utf-8" 36 | 37 | 38 | session-pools: 39 | chat: 40 | listen: [127.0.0.1:8081] 41 | inactivity-handlers: [chat] 42 | 43 | 44 | http-destinations: 45 | chat: 46 | override-host-header: swindon.internal 47 | addresses: 48 | - 127.0.0.1:8082 49 | -------------------------------------------------------------------------------- /examples/presence/swindon1.yaml: -------------------------------------------------------------------------------- 1 | listen: 2 | - 127.0.0.2:8080 3 | 4 | debug-routing: true 5 | 6 | routing: 7 | "*": chat 8 | "*/css": public 9 | "*/empty.gif": empty-gif 10 | "*/js": public 11 | "*/~~swindon-status": status 12 | 13 | handlers: 14 | 15 | chat: !SwindonLattice 16 | 17 | session-pool: chat 18 | http-route: html 19 | compatibility: v0.6.2 20 | 21 | message-handlers: 22 | "*": chat 23 | 24 | empty-gif: !EmptyGif 25 | status: !SelfStatus 26 | 27 | public: !Static 28 | mode: relative_to_domain_root 29 | path: ./public 30 | text-charset: utf-8 31 | 32 | html: !SingleFile 33 | path: ./public/index.html 34 | content-type: "text/html; charset=utf-8" 35 | 36 | 37 | session-pools: 38 | chat: 39 | listen: [127.0.0.1:8081] 40 | inactivity-handlers: [chat] 41 | 42 | 43 | http-destinations: 44 | chat: 45 | override-host-header: swindon.internal 46 | addresses: 47 | - 127.0.0.1:8082 48 | 49 | replication: 50 | listen: 51 | - 127.0.0.2:7878 52 | peers: 53 | - 127.0.0.3:7878 54 | -------------------------------------------------------------------------------- /examples/presence/swindon2.yaml: -------------------------------------------------------------------------------- 1 | listen: 2 | - 127.0.0.3:8080 3 | 4 | debug-routing: true 5 | 6 | routing: 7 | "*": chat 8 | "*/css": public 9 | "*/empty.gif": empty-gif 10 | "*/js": public 11 | "*/~~swindon-status": status 12 | 13 | handlers: 14 | 15 | chat: !SwindonLattice 16 | 17 | session-pool: chat 18 | http-route: html 19 | compatibility: v0.6.2 20 | 21 | message-handlers: 22 | "*": chat 23 | 24 | empty-gif: !EmptyGif 25 | status: !SelfStatus 26 | 27 | public: !Static 28 | mode: relative_to_domain_root 29 | path: ./public 30 | text-charset: utf-8 31 | 32 | html: !SingleFile 33 | path: ./public/index.html 34 | content-type: "text/html; charset=utf-8" 35 | 36 | 37 | session-pools: 38 | chat: 39 | listen: [127.0.0.1:8091] 40 | inactivity-handlers: [chat] 41 | 42 | 43 | http-destinations: 44 | chat: 45 | override-host-header: swindon.internal 46 | addresses: 47 | - 127.0.0.1:8082 48 | 49 | replication: 50 | listen: 51 | - 127.0.0.3:7878 52 | peers: 53 | - 127.0.0.2:7878 54 | -------------------------------------------------------------------------------- /examples/presence/user_list.marko: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie' 2 | import {Swindon, Lattice} from 'swindon' 3 | 4 | class { 5 | onCreate() { 6 | var {swindon_presence_login} = cookie.parse(document.cookie) 7 | this.swindon = new Swindon('ws://' + location.host) 8 | this.state = { 9 | login: swindon_presence_login, 10 | lattice: new Lattice({onUpdate: this.lattice_update.bind(this)}), 11 | } 12 | } 13 | onMount() { 14 | this.guard = this.swindon.guard() 15 | .lattice('swindon.user', '', this.state.lattice) 16 | } 17 | onUnmount() { 18 | this.guard.close() 19 | } 20 | lattice_update(keys, lat) { 21 | this.forceUpdate() 22 | } 23 | } 24 |

Logged in as: ${state.login}

25 |

Other users:

26 |
    27 |
  • 28 | ${user}: ${state.lattice.getRegister(user, 'status')[1]} 29 |
  • 30 |
31 | -------------------------------------------------------------------------------- /examples/presence/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | output: { 6 | filename: 'bundle.js', 7 | path: path.resolve(__dirname, 'public/js') 8 | }, 9 | module: { 10 | rules: [{ 11 | loader: 'babel-loader', 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | }, { 15 | loader: 'marko-loader', 16 | test: /\.marko$/, 17 | }, { 18 | loader: 'css-loader', 19 | test: /\.css$/, 20 | }], 21 | }, 22 | resolve: { 23 | modules: process.env.NODE_PATH.split(':').concat('node_modules'), 24 | extensions: ['.js', '.marko'], 25 | mainFields: ['browser', 'jsnext:main', 'main'], 26 | }, 27 | resolveLoader: { 28 | modules: process.env.NODE_PATH.split(':').concat('node_modules'), 29 | mainFields: ['jsnext:main', 'main'], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /public/websocket.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebSocket Test 4 | 59 |

WebSocket Test

60 | 61 | 62 |
63 | 64 | -------------------------------------------------------------------------------- /src/authorizers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod source_ip; 2 | -------------------------------------------------------------------------------- /src/authorizers/source_ip.rs: -------------------------------------------------------------------------------- 1 | use std::str::from_utf8; 2 | use std::sync::Arc; 3 | use std::net::IpAddr; 4 | 5 | use tk_http::server::{Error}; 6 | 7 | use crate::config::networks::SourceIpAuthorizer; 8 | use crate::incoming::Input; 9 | 10 | 11 | pub fn check(cfg: &Arc, input: &mut Input) 12 | -> Result 13 | { 14 | let forwarded = cfg.accept_forwarded_headers_from.as_ref() 15 | .and_then(|netw| input.config.networks.get(netw)) 16 | .map(|netw| { 17 | if let Some(subnet) = netw.get_subnet(input.addr.ip()) { 18 | input.debug.add_allow( 19 | format_args!("forwarded-from {}", subnet)); 20 | true 21 | } else { 22 | false 23 | } 24 | }) 25 | .unwrap_or(false); 26 | let ip = match (&cfg.forwarded_ip_header, forwarded) { 27 | (&Some(ref header), true) => { 28 | let mut ip = None; 29 | for (name, value) in input.headers.headers() { 30 | if name.eq_ignore_ascii_case(header) { 31 | let parsed = from_utf8(value).ok() 32 | .and_then(|x| x.parse::().ok()); 33 | match parsed { 34 | Some(parsed) => ip = Some(parsed), 35 | None => { 36 | debug!("Invalid ip {:?} from header {}", 37 | String::from_utf8_lossy(value), name); 38 | input.debug.set_deny( 39 | "invalid-source-ip-from-header"); 40 | // TODO(tailhook) consider returning error 41 | return Ok(false); 42 | } 43 | } 44 | } 45 | } 46 | ip.unwrap_or(input.addr.ip()) 47 | } 48 | _ => input.addr.ip(), 49 | }; 50 | if let Some(netw) = input.config.networks.get(&cfg.allowed_network) { 51 | if let Some(subnet) = netw.get_subnet(ip) { 52 | input.debug.add_allow(format_args!("source-ip {}", subnet)); 53 | Ok(true) 54 | } else { 55 | input.debug.set_deny(format!("source-ip {}", ip)); 56 | Ok(false) 57 | } 58 | } else { 59 | input.debug.set_deny(format!("no-network {}", cfg.allowed_network)); 60 | Ok(false) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/chat/cid.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::str::FromStr; 3 | use std::num::ParseIntError; 4 | use serde::de::{self, Deserialize, Deserializer, Visitor}; 5 | 6 | use crate::runtime::ServerId; 7 | 8 | /// Internal connection id 9 | #[derive(Hash, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] 10 | pub struct Cid(u64); 11 | 12 | 13 | /// Public connection id 14 | pub struct PubCid(pub Cid, pub ServerId); 15 | 16 | impl Cid { 17 | pub fn new() -> Cid { 18 | // Until atomic u64 really works 19 | use std::sync::atomic::{AtomicU64, Ordering}; 20 | static COUNTER: AtomicU64 = AtomicU64::new(0); 21 | Cid(COUNTER.fetch_add(1, Ordering::Relaxed)) 22 | } 23 | } 24 | 25 | 26 | impl FromStr for Cid { 27 | type Err = ParseIntError; 28 | 29 | fn from_str(src: &str) -> Result { 30 | src.parse().map(|x| Cid(x)) 31 | } 32 | } 33 | 34 | impl fmt::Debug for Cid { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | if f.alternate() { 37 | write!(f, "cid:{}", self.0) 38 | } else { 39 | write!(f, "Cid({})", self.0) 40 | } 41 | } 42 | } 43 | 44 | impl fmt::Display for Cid { 45 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 46 | write!(f, "{}", self.0) 47 | } 48 | } 49 | 50 | impl FromStr for PubCid { 51 | type Err = (); 52 | 53 | fn from_str(src: &str) -> Result { 54 | let s = src.rfind('-').ok_or(())?; 55 | let (rid, cid) = src.split_at(s); 56 | let rid = rid.parse().map_err(|_| ())?; 57 | let cid = cid[1..].parse().map_err(|_| ())?; 58 | Ok(PubCid(cid, rid)) 59 | } 60 | } 61 | 62 | impl<'de> Deserialize<'de> for PubCid { 63 | fn deserialize(d: D) -> Result 64 | where D: Deserializer<'de> 65 | { 66 | d.deserialize_str(CidVisitor) 67 | } 68 | } 69 | 70 | struct CidVisitor; 71 | 72 | impl<'de> Visitor<'de> for CidVisitor { 73 | type Value = PubCid; 74 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 75 | write!(f, "valid connection id string") 76 | } 77 | fn visit_str(self, val: &str) -> Result 78 | where E: de::Error 79 | { 80 | val.parse().map_err(|_| de::Error::custom("invalid connection id")) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/chat/close_reason.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq)] 2 | pub enum CloseReason { 3 | /// Stopping websocket because respective session pool is stopped 4 | PoolStopped, 5 | /// Closed by peer, we just propagate the message here 6 | PeerClose(u16, String), 7 | } 8 | -------------------------------------------------------------------------------- /src/chat/connection_sender.rs: -------------------------------------------------------------------------------- 1 | use futures::sync::mpsc::{unbounded, UnboundedSender, UnboundedReceiver}; 2 | 3 | use crate::chat::{ConnectionMessage}; 4 | 5 | pub type Receiver = UnboundedReceiver; 6 | 7 | #[derive(Clone)] 8 | pub struct ConnectionSender { 9 | sender: UnboundedSender, 10 | } 11 | 12 | impl ConnectionSender { 13 | pub fn new() -> (ConnectionSender, Receiver) { 14 | let (tx, rx) = unbounded(); 15 | (ConnectionSender { 16 | sender: tx, 17 | }, rx) 18 | } 19 | pub fn send(&self, msg: ConnectionMessage) { 20 | self.sender.unbounded_send(msg) 21 | .map_err(|e| debug!("Error sending connection message: {}. \ 22 | usually these means connection has been closed to soon", e)).ok(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/chat/listener/inactivity_handler.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/src/chat/listener/inactivity_handler.rs -------------------------------------------------------------------------------- /src/chat/listener/mod.rs: -------------------------------------------------------------------------------- 1 | mod spawn; 2 | mod codec; 3 | mod pools; 4 | 5 | pub use self::pools::SessionPools; 6 | -------------------------------------------------------------------------------- /src/chat/listener/spawn.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::stream::Stream; 4 | use tk_http; 5 | use tk_http::server::Proto; 6 | use tk_listen::{BindMany, ListenExt}; 7 | use futures::future::{Future}; 8 | use tokio_core::reactor::{Handle}; 9 | use ns_router::future::AddrStream; 10 | 11 | use crate::intern::SessionPoolName; 12 | use crate::config::SessionPool; 13 | use crate::runtime::Runtime; 14 | use crate::chat::listener::codec::Handler; 15 | use crate::chat::processor::{ProcessorPool}; 16 | use crate::chat::replication::RemotePool; 17 | 18 | 19 | pub struct WorkerData { 20 | pub name: SessionPoolName, 21 | pub runtime: Arc, 22 | pub settings: Arc, 23 | pub processor: ProcessorPool, 24 | pub remote: RemotePool, 25 | 26 | pub handle: Handle, // Does it belong here? 27 | } 28 | 29 | pub fn listen(addr_stream: AddrStream, worker_data: &Arc) { 30 | let w1 = worker_data.clone(); 31 | let w2 = worker_data.clone(); 32 | let runtime = worker_data.runtime.clone(); 33 | let h1 = runtime.handle.clone(); 34 | 35 | // TODO(tailhook) how to update? 36 | let hcfg = tk_http::server::Config::new() 37 | .inflight_request_limit(worker_data.settings.pipeline_depth) 38 | // TODO(tailhook) make it configurable? 39 | .inflight_request_prealoc(0) 40 | .done(); 41 | 42 | worker_data.handle.spawn( 43 | BindMany::new(addr_stream.map(|addr| addr.addresses_at(0)), &h1) 44 | .sleep_on_error(w1.settings.listen_error_timeout, &runtime.handle) 45 | .map(move |(socket, saddr)| { 46 | Proto::new(socket, &hcfg, Handler::new(saddr, w2.clone()), &h1) 47 | .map_err(|e| debug!("Chat backend protocol error: {}", e)) 48 | }) 49 | .listen(worker_data.settings.max_connections) 50 | .map(move |()| error!("Replication listener exited")) 51 | .map_err(move |()| error!("Replication listener errored")) 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/chat/processor/connection.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::collections::{HashSet, HashMap}; 3 | 4 | use serde_json::Value as Json; 5 | 6 | use crate::chat::{Cid, CloseReason, ConnectionSender}; 7 | use crate::intern::{Topic, SessionId, Lattice as Namespace, LatticeKey}; 8 | use super::{ConnectionMessage}; 9 | use super::lattice; 10 | 11 | 12 | pub struct NewConnection { 13 | pub cid: Cid, 14 | pub topics: HashSet, 15 | pub lattices: HashSet, 16 | pub users_lattice: HashSet, 17 | pub message_buffer: Vec<(Topic, Arc)>, 18 | pub channel: ConnectionSender, 19 | } 20 | 21 | 22 | pub struct Connection { 23 | pub cid: Cid, 24 | pub session_id: SessionId, 25 | pub topics: HashSet, 26 | pub lattices: HashSet, 27 | pub users_lattice: bool, 28 | pub channel: ConnectionSender, 29 | } 30 | 31 | impl NewConnection { 32 | pub fn new(conn_id: Cid, channel: ConnectionSender) 33 | -> NewConnection 34 | { 35 | NewConnection { 36 | cid: conn_id, 37 | topics: HashSet::new(), 38 | lattices: HashSet::new(), 39 | users_lattice: HashSet::new(), 40 | message_buffer: Vec::new(), 41 | channel: channel, 42 | } 43 | } 44 | pub fn associate(self, session_id: SessionId) 45 | -> (Connection, HashSet) 46 | { 47 | let mut conn = Connection { 48 | cid: self.cid, 49 | session_id: session_id, 50 | topics: self.topics, 51 | lattices: self.lattices, 52 | users_lattice: self.users_lattice.len() > 0, 53 | channel: self.channel, 54 | }; 55 | for (t, m) in self.message_buffer { 56 | conn.message(t, m); 57 | } 58 | return (conn, self.users_lattice); 59 | } 60 | pub fn message(&mut self, topic: Topic, data: Arc) { 61 | self.message_buffer.push((topic, data)); 62 | } 63 | pub fn stop(&mut self, reason: CloseReason) { 64 | self.channel.send(ConnectionMessage::StopSock(reason)); 65 | } 66 | } 67 | 68 | impl Connection { 69 | 70 | pub fn message(&mut self, topic: Topic, data: Arc) { 71 | self.channel.send(ConnectionMessage::Publish(topic, data)); 72 | } 73 | 74 | pub fn lattice(&mut self, namespace: &Namespace, 75 | update: &Arc>) 76 | { 77 | let msg = ConnectionMessage::Lattice( 78 | namespace.clone(), update.clone()); 79 | self.channel.send(msg); 80 | } 81 | pub fn stop(&mut self, reason: CloseReason) { 82 | self.channel.send(ConnectionMessage::StopSock(reason)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/chat/processor/pair.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::hash::Hash; 3 | 4 | 5 | pub trait PairCollection { 6 | fn insert_clone(&mut self, key1: &A, key2: &B) 7 | where A: Clone, B: Clone; 8 | fn insert_list_key1<'x, I>(&mut self, key1list: I, key2: &B) 9 | where I: IntoIterator, 10 | A: Clone + 'x, 11 | B: Clone 12 | { 13 | for key1 in key1list { 14 | self.insert_clone(key1, key2); 15 | } 16 | } 17 | fn remove(&mut self, key1: &A, key2: &B); 18 | fn remove_list_key1<'x, I>(&mut self, key1list: I, key2: &B) 19 | where I: IntoIterator, 20 | A: 'x 21 | { 22 | for key1 in key1list { 23 | self.remove(key1, key2); 24 | } 25 | } 26 | } 27 | 28 | impl PairCollection for HashMap> 29 | where A: Eq + Hash, 30 | B: Eq + Hash, 31 | { 32 | fn insert_clone(&mut self, key1: &A, key2: &B) 33 | where A: Clone, B: Clone 34 | { 35 | self.entry(key1.clone()) 36 | .or_insert_with(HashSet::new) 37 | .insert(key2.clone()); 38 | } 39 | fn remove(&mut self, key1: &A, key2: &B) { 40 | let len = if let Some(sub) = self.get_mut(key1) { 41 | sub.remove(key2); 42 | sub.len() 43 | } else { 44 | return; 45 | }; 46 | if len == 0 { 47 | self.remove(key1); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/chat/processor/session.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::collections::{HashSet, HashMap}; 3 | use std::time::SystemTime; 4 | 5 | use serde_json::Value as Json; 6 | 7 | use crate::chat::Cid; 8 | use crate::intern::{Lattice, SessionId}; 9 | 10 | pub struct UsersLattice { 11 | pub(in crate::chat::processor) connections: HashSet, 12 | pub(in crate::chat::processor) peers: HashSet, 13 | } 14 | 15 | pub struct Session { 16 | pub(in crate::chat::processor) status_timestamp: SystemTime, 17 | pub(in crate::chat::processor) connections: HashSet, 18 | pub(in crate::chat::processor) lattices: HashMap>, 19 | pub(in crate::chat::processor) users_lattice: UsersLattice, 20 | pub(in crate::chat::processor) metadata: Arc, 21 | } 22 | 23 | impl Session { 24 | pub fn new() -> Session { 25 | Session { 26 | status_timestamp: SystemTime::now(), 27 | connections: HashSet::new(), 28 | lattices: HashMap::new(), 29 | users_lattice: UsersLattice { 30 | connections: HashSet::new(), 31 | peers: HashSet::new(), 32 | }, 33 | metadata: Arc::new(json!({})), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/chat/processor/try_iter.rs: -------------------------------------------------------------------------------- 1 | //! This code if from https://github.com/rust-lang/rust/pull/34724/files 2 | //! 3 | //! It's here until try_iter will be stable in rust 4 | use std::sync::mpsc::Receiver; 5 | 6 | 7 | /// An iterator that attempts to yield all pending values for a receiver. 8 | /// `None` will be returned when there are no pending values remaining or 9 | /// if the corresponding channel has hung up. 10 | /// 11 | /// This Iterator will never block the caller in order to wait for data to 12 | /// become available. Instead, it will return `None`. 13 | pub struct TryIter<'a, T: 'a> { 14 | rx: &'a Receiver 15 | } 16 | 17 | /// Returns an iterator that will attempt to yield all pending values. 18 | /// It will return `None` if there are no more pending values or if the 19 | /// channel has hung up. The iterator will never `panic!` or block the 20 | /// user by waiting for values. 21 | pub fn try_iter(recv: &Receiver) -> TryIter { 22 | TryIter { rx: recv } 23 | } 24 | 25 | impl<'a, T> Iterator for TryIter<'a, T> { 26 | type Item = T; 27 | 28 | fn next(&mut self) -> Option { self.rx.try_recv().ok() } 29 | } 30 | -------------------------------------------------------------------------------- /src/chat/replication/client.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | use tk_http::websocket::client::{self as ws, Head, Encoder, EncoderDone}; 3 | use tk_http::websocket::Error; 4 | 5 | use crate::runtime::ServerId; 6 | 7 | 8 | pub struct Authorizer { 9 | server_id: ServerId, 10 | peername: String, 11 | } 12 | 13 | 14 | impl Authorizer { 15 | pub fn new(peer: String, server_id: ServerId) 16 | -> Authorizer 17 | { 18 | Authorizer { 19 | server_id: server_id, 20 | peername: peer, 21 | } 22 | } 23 | } 24 | 25 | impl ws::Authorizer for Authorizer { 26 | type Result = ServerId; 27 | 28 | fn write_headers(&mut self, mut e: Encoder) 29 | -> EncoderDone 30 | { 31 | e.request_line("/v1/swindon-chat"); 32 | e.format_header("Host", &self.peername).unwrap(); 33 | e.format_header("Origin", 34 | format_args!("http://{}/v1/swindon-chat", self.peername)).unwrap(); 35 | e.format_header("X-Swindon-Node-Id", &self.server_id).unwrap(); 36 | e.done() 37 | } 38 | 39 | fn headers_received(&mut self, headers: &Head) 40 | -> Result 41 | { 42 | headers.all_headers().iter() 43 | .find(|h| h.name.eq_ignore_ascii_case("X-Swindon-Node-Id")) 44 | .ok_or(Error::custom("missing X-Swindon-Node-Id header")) 45 | .and_then(|h| str::from_utf8(h.value) 46 | .map_err(|_| Error::custom("invalid node id"))) 47 | .and_then(|s| s.parse() 48 | .map_err(|_| Error::custom("invalid node id"))) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/chat/replication/mod.rs: -------------------------------------------------------------------------------- 1 | use futures::sync::mpsc::UnboundedSender; 2 | use tk_http::websocket::Packet; 3 | 4 | use crate::metrics::{Counter, Integer}; 5 | 6 | mod action; 7 | mod session; 8 | mod spawn; 9 | mod server; 10 | mod client; 11 | 12 | pub use self::action::{ReplAction, RemoteAction}; 13 | pub use self::session::ReplicationSession; 14 | pub use self::session::{RemoteSender, RemotePool}; 15 | 16 | pub type IncomingChannel = UnboundedSender; 17 | pub type OutgoingChannel = UnboundedSender; 18 | 19 | lazy_static! { 20 | pub static ref CONNECTIONS: Integer = Integer::new(); 21 | pub static ref FRAMES_SENT: Counter = Counter::new(); 22 | pub static ref FRAMES_RECEIVED: Counter = Counter::new(); 23 | } 24 | -------------------------------------------------------------------------------- /src/chat/tangle_auth.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde_json::to_string; 4 | 5 | use crate::base64::Base64; 6 | use crate::intern::SessionId; 7 | 8 | pub struct TangleAuth<'a>(pub &'a SessionId); 9 | 10 | impl<'a> fmt::Display for TangleAuth<'a> { 11 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 12 | #[derive(Serialize)] 13 | struct Auth<'a> { 14 | user_id: &'a SessionId, 15 | } 16 | write!(f, "Tangle {}", Base64(to_string(&Auth { 17 | user_id: self.0, 18 | }).unwrap().as_bytes())) 19 | } 20 | } 21 | 22 | pub struct SwindonAuth<'a>(pub &'a SessionId); 23 | 24 | impl<'a> fmt::Display for SwindonAuth<'a> { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | #[derive(Serialize)] 27 | struct Auth<'a> { 28 | user_id: &'a SessionId, 29 | } 30 | write!(f, "Swindon+json {}", Base64(to_string(&Auth { 31 | user_id: self.0, 32 | }).unwrap().as_bytes())) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/config/authorizers.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use quire::validate::{Enum, Nothing}; 4 | 5 | use crate::config::ldap; 6 | use crate::config::networks; 7 | 8 | 9 | #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] 10 | pub enum Authorizer { 11 | AllowAll, 12 | SourceIp(Arc), 13 | Ldap(Arc), 14 | } 15 | 16 | pub fn validator<'x>() -> Enum<'x> { 17 | Enum::new() 18 | .option("AllowAll", Nothing) 19 | .option("Ldap", ldap::authorizer_validator()) 20 | .option("SourceIp", networks::source_ip_authorizer_validator()) 21 | } 22 | -------------------------------------------------------------------------------- /src/config/disk.rs: -------------------------------------------------------------------------------- 1 | use quire::validate::{Structure, Numeric}; 2 | 3 | #[derive(Deserialize, Debug, PartialEq, Eq, Hash)] 4 | pub struct Disk { 5 | pub num_threads: usize, 6 | } 7 | 8 | pub fn validator<'x>() -> Structure<'x> { 9 | Structure::new() 10 | .member("num_threads", Numeric::new().min(1)) 11 | } 12 | -------------------------------------------------------------------------------- /src/config/empty_gif.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use quire::validate::{Structure, Mapping, Scalar}; 4 | use crate::config::static_files::header_contains; 5 | use serde::de::{Deserializer, Deserialize}; 6 | 7 | 8 | #[derive(Debug, PartialEq, Eq)] 9 | pub struct EmptyGif { 10 | pub extra_headers: HashMap, 11 | // Computed values 12 | pub overrides_content_type: bool, 13 | } 14 | 15 | pub fn validator<'x>() -> Structure<'x> { 16 | Structure::new() 17 | .member("extra_headers", Mapping::new(Scalar::new(), Scalar::new())) 18 | } 19 | 20 | impl<'a> Deserialize<'a> for EmptyGif { 21 | fn deserialize>(d: D) -> Result { 22 | #[derive(Deserialize)] 23 | pub struct Internal { 24 | pub extra_headers: HashMap, 25 | } 26 | let int = Internal::deserialize(d)?; 27 | return Ok(EmptyGif { 28 | overrides_content_type: 29 | header_contains(&int.extra_headers, "Content-Type"), 30 | extra_headers: int.extra_headers, 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/config/fingerprint.rs: -------------------------------------------------------------------------------- 1 | use std::default::Default; 2 | use std::io::{self, Write}; 3 | use std::fs::{File, Metadata}; 4 | use std::path::PathBuf; 5 | 6 | use blake2::Blake2b; 7 | use digest::FixedOutput; 8 | use digest_writer::Writer; 9 | use generic_array::GenericArray; 10 | use typenum::U64; 11 | 12 | pub type Fingerprint = GenericArray; 13 | 14 | 15 | #[cfg(unix)] 16 | pub fn compare_metadata(meta: &Metadata, old_meta: &Metadata) -> bool { 17 | use std::os::unix::fs::MetadataExt; 18 | meta.modified().ok() != old_meta.modified().ok() || 19 | meta.ino() != old_meta.ino() || 20 | meta.dev() != old_meta.dev() 21 | } 22 | 23 | #[cfg(not(unix))] 24 | pub fn compare_metadata(meta: &Metadata, old_meta: &Metadata) -> bool { 25 | meta.modified().ok() != old_meta.modified().ok() 26 | } 27 | 28 | pub fn calc(meta: &Vec<(PathBuf, String, Metadata)>) 29 | -> Result 30 | { 31 | let mut digest = Writer::new(Blake2b::default()); 32 | for &(ref filename, ref name, ref meta) in meta { 33 | let mut file = File::open(filename)?; 34 | if compare_metadata(&file.metadata()?, meta) { 35 | return Err(io::ErrorKind::Interrupted.into()); 36 | } 37 | digest.write(name.as_bytes())?; 38 | digest.write(&[0])?; 39 | io::copy(&mut file, &mut digest)?; 40 | } 41 | Ok(digest.into_inner().fixed_result()) 42 | } 43 | -------------------------------------------------------------------------------- /src/config/handlers.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use quire::validate::{Enum, Nothing}; 4 | 5 | use super::chat; 6 | use super::empty_gif; 7 | use super::proxy; 8 | use super::redirect; 9 | use super::self_status; 10 | use super::static_files; 11 | 12 | 13 | #[derive(Deserialize, Debug, PartialEq, Eq, Clone)] 14 | pub enum Handler { 15 | SwindonLattice(Arc), 16 | Static(Arc), 17 | SingleFile(Arc), 18 | VersionedStatic(Arc), 19 | Proxy(Arc), 20 | EmptyGif(Arc), 21 | NotFound, 22 | HttpBin, 23 | /// This endpoints is for testing websocket implementation. It's not 24 | /// guaranteed to work in forward compatible manner. We use it for 25 | /// autobahn tests, but we might choose to change test suite, so don't use 26 | /// it for something serious. 27 | WebsocketEcho, 28 | BaseRedirect(Arc), 29 | StripWWWRedirect, 30 | SelfStatus(Arc), 31 | } 32 | 33 | pub fn validator<'x>() -> Enum<'x> { 34 | Enum::new() 35 | .option("SwindonLattice", chat::validator()) 36 | .option("Static", static_files::validator()) 37 | .option("SingleFile", static_files::single_file()) 38 | .option("VersionedStatic", static_files::versioned_validator()) 39 | .option("Proxy", proxy::validator()) 40 | .option("HttpBin", Nothing) 41 | .option("EmptyGif", empty_gif::validator()) 42 | .option("WebsocketEcho", Nothing) 43 | .option("BaseRedirect", redirect::base_redirect()) 44 | .option("StripWWWRedirect", Nothing) 45 | .option("SelfStatus", self_status::validator()) 46 | } 47 | -------------------------------------------------------------------------------- /src/config/http.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use serde::de::{Deserializer, Deserialize}; 4 | use quire::validate::{Scalar}; 5 | 6 | use crate::intern::Upstream; 7 | use crate::config::visitors::FromStrVisitor; 8 | 9 | 10 | pub fn destination_validator() -> Scalar { 11 | Scalar::new() 12 | } 13 | 14 | #[derive(PartialEq, Eq, Debug, Clone)] 15 | pub struct Destination { 16 | pub upstream: Upstream, 17 | pub path: String, 18 | } 19 | 20 | impl<'a> Deserialize<'a> for Destination { 21 | fn deserialize>(d: D) -> Result { 22 | d.deserialize_str(FromStrVisitor::new("upstream/path")) 23 | } 24 | } 25 | 26 | impl FromStr for Destination { 27 | type Err = String; 28 | fn from_str(val: &str) -> Result { 29 | if let Some(path_start) = val.find('/') { 30 | Ok(Destination { 31 | upstream: Upstream::from_str(&val[..path_start]) 32 | .map_err(|e| format!("Invalid upstream: {}", e))?, 33 | path: val[path_start..].to_string(), 34 | }) 35 | } else { 36 | Ok(Destination { 37 | upstream: Upstream::from_str(&val) 38 | .map_err(|e| format!("Invalid upstream: {}", e))?, 39 | path: "/".to_string(), 40 | }) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/config/http_destinations.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use quire::validate::{Structure, Scalar, Enum, Numeric, Nothing}; 4 | use quire::validate::{Sequence}; 5 | 6 | #[derive(Deserialize, Debug, PartialEq, Eq)] 7 | #[allow(non_camel_case_types)] 8 | pub enum LoadBalancing { 9 | queue, 10 | } 11 | 12 | #[derive(Deserialize, Debug, PartialEq, Eq)] 13 | pub struct Destination { 14 | pub load_balancing: LoadBalancing, 15 | pub queue_size_for_503: usize, 16 | pub backend_connections_per_ip_port: u32, 17 | pub in_flight_requests_per_backend_connection: usize, 18 | pub addresses: Vec, 19 | #[serde(with="::quire::duration")] 20 | pub keep_alive_timeout: Duration, 21 | #[serde(with="::quire::duration")] 22 | pub max_request_timeout: Duration, 23 | #[serde(with="::quire::duration")] 24 | pub safe_pipeline_timeout: Duration, 25 | pub override_host_header: Option, 26 | pub request_id_header: Option, 27 | } 28 | 29 | pub fn validator<'x>() -> Structure<'x> { 30 | Structure::new() 31 | .member("load_balancing", Enum::new() 32 | .option("queue", Nothing) 33 | .allow_plain() 34 | .plain_default("queue")) 35 | .member("queue_size_for_503", 36 | Numeric::new().min(0).max(1 << 32).default(100_000)) 37 | .member("backend_connections_per_ip_port", 38 | Numeric::new().min(1).max(100_000).default(100)) 39 | .member("in_flight_requests_per_backend_connection", 40 | Numeric::new().min(1).max(1000).default(2)) 41 | .member("addresses", Sequence::new(Scalar::new()).min_length(1)) 42 | .member("keep_alive_timeout", Scalar::new().default("4 sec")) 43 | .member("max_request_timeout", Scalar::new().default("30 secs")) 44 | .member("safe_pipeline_timeout", Scalar::new().default("300 ms")) 45 | .member("override_host_header", Scalar::new().optional()) 46 | .member("request_id_header", Scalar::new().optional()) 47 | } 48 | -------------------------------------------------------------------------------- /src/config/ldap.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use quire::validate::{Structure, Sequence, Mapping, Scalar}; 4 | 5 | use crate::intern::LdapUpstream; 6 | 7 | 8 | #[derive(Deserialize, PartialEq, Eq, Debug)] 9 | pub struct Destination { 10 | pub addresses: Vec, 11 | } 12 | 13 | #[derive(Deserialize, Debug, PartialEq, Eq)] 14 | pub struct Query { 15 | pub search_base: String, 16 | pub fetch_attribute: String, 17 | pub filter: String, 18 | pub dn_attribute_strip_base: Option, 19 | } 20 | 21 | #[derive(Deserialize, Debug, PartialEq, Eq)] 22 | pub struct Ldap { 23 | pub destination: LdapUpstream, 24 | pub search_base: String, 25 | pub login_attribute: String, 26 | pub password_attribute: String, 27 | pub login_header: Option, 28 | pub additional_queries: HashMap, 29 | } 30 | 31 | 32 | pub fn destination_validator<'x>() -> Structure<'x> { 33 | Structure::new() 34 | .member("addresses", Sequence::new(Scalar::new()).min_length(1)) 35 | } 36 | 37 | pub fn authorizer_validator<'x>() -> Structure<'x> { 38 | Structure::new() 39 | .member("destination", Scalar::new()) 40 | .member("search_base", Scalar::new()) 41 | .member("login_attribute", Scalar::new()) 42 | .member("password_attribute", Scalar::new()) 43 | .member("login_header", Scalar::new().optional()) 44 | .member("additional_queries", Mapping::new( 45 | Scalar::new(), 46 | Structure::new() 47 | .member("search_base", Scalar::new()) 48 | .member("fetch_attribute", Scalar::new()) 49 | .member("filter", Scalar::new()) 50 | .member("dn_attribute_strip_base", Scalar::new().optional()))) 51 | } 52 | -------------------------------------------------------------------------------- /src/config/listen.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ns_router::AutoName; 4 | use quire::validate::{Enum, Scalar}; 5 | 6 | 7 | #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] 8 | pub enum ListenSocket { 9 | Tcp(String), 10 | // TODO(tailhook) 11 | // Fd(u32) 12 | // Unix(PathBuf) 13 | } 14 | 15 | #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] 16 | pub struct Listen(Arc>); 17 | 18 | impl Listen { 19 | pub fn new(vec: Vec) -> Listen { 20 | Listen(Arc::new(vec)) 21 | } 22 | pub fn len(&self) -> usize { 23 | self.0.len() 24 | } 25 | } 26 | 27 | impl<'a> IntoIterator for &'a Listen { 28 | type Item = AutoName<'a>; 29 | type IntoIter = ::std::iter::Map<::std::slice::Iter<'a, ListenSocket>, 30 | fn(&'a ListenSocket) -> AutoName<'a>>; 31 | fn into_iter(self) -> Self::IntoIter { 32 | self.0.iter().map(|x| match *x { 33 | ListenSocket::Tcp(ref s) => AutoName::Auto(s), 34 | }) 35 | } 36 | } 37 | 38 | pub fn validator<'x>() -> Enum<'x> { 39 | Enum::new() 40 | .option("Tcp", Scalar::new()) 41 | .default_tag("Tcp") 42 | } 43 | -------------------------------------------------------------------------------- /src/config/log.rs: -------------------------------------------------------------------------------- 1 | use trimmer::{Template, Options, ParseError}; 2 | use serde::de::{Deserialize, Deserializer, Error}; 3 | use quire::validate::{Structure, Scalar}; 4 | 5 | use crate::template; 6 | 7 | lazy_static! { 8 | static ref OPTIONS: Options = Options::new() 9 | .syntax_oneline() 10 | .clone(); 11 | } 12 | 13 | 14 | #[derive(Debug)] 15 | pub struct Format { 16 | pub template_source: String, 17 | pub template: Template, 18 | } 19 | 20 | impl<'a> Deserialize<'a> for Format { 21 | fn deserialize>(d: D) -> Result { 22 | #[derive(Deserialize)] 23 | struct FormatRaw { 24 | template: String, 25 | } 26 | let raw = FormatRaw::deserialize(d)?; 27 | Format::from_string(raw.template) 28 | .map_err(|e| D::Error::custom(&format!("{}", e))) 29 | } 30 | } 31 | 32 | impl PartialEq for Format { 33 | fn eq(&self, other: &Format) -> bool { 34 | self.template_source == other.template_source 35 | } 36 | } 37 | 38 | impl Eq for Format { } 39 | 40 | pub fn format_validator<'x>() -> Structure<'x> { 41 | Structure::new() 42 | .member("template", Scalar::new()) 43 | } 44 | 45 | impl Format { 46 | pub fn from_string(template: String) -> Result { 47 | Ok(Format { 48 | template: template::PARSER 49 | .parse_with_options(&*OPTIONS, &template)?, 50 | template_source: template, 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/config/proxy.rs: -------------------------------------------------------------------------------- 1 | use super::http; 2 | 3 | use quire::validate::{Nothing, Enum, Structure, Scalar, Numeric}; 4 | 5 | #[derive(Deserialize, Debug, PartialEq, Eq)] 6 | #[allow(non_camel_case_types)] 7 | pub enum Mode { 8 | /// Means forward all headers including Host header 9 | forward, 10 | } 11 | 12 | #[derive(Deserialize, Debug, PartialEq, Eq)] 13 | pub struct Proxy { 14 | pub mode: Mode, 15 | pub ip_header: Option, 16 | // NOTE: option is deprecated. 17 | pub request_id_header: Option, 18 | pub destination: http::Destination, 19 | // TODO(tailhook) this might needs to be u64 20 | pub max_payload_size: usize, 21 | pub stream_requests: bool, 22 | pub response_buffer_size: usize, 23 | } 24 | 25 | pub fn validator<'x>() -> Structure<'x> { 26 | Structure::new() 27 | .member("mode", Enum::new() 28 | .option("forward", Nothing) 29 | .allow_plain() 30 | .plain_default("forward")) 31 | .member("ip_header", Scalar::new().optional()) 32 | .member("request_id_header", Scalar::new().optional()) 33 | .member("max_payload_size", 34 | Numeric::new().min(0).max(1 << 40).default(10 << 20)) 35 | .member("stream_requests", Scalar::new().default(false)) 36 | .member("response_buffer_size", 37 | Numeric::new().min(0).max(1 << 40).default(10 << 20)) 38 | .member("destination", http::destination_validator()) 39 | } 40 | -------------------------------------------------------------------------------- /src/config/redirect.rs: -------------------------------------------------------------------------------- 1 | use quire::validate::{Structure, Scalar}; 2 | 3 | 4 | #[derive(Deserialize, Debug, PartialEq, Eq)] 5 | pub struct BaseRedirect { 6 | pub redirect_to_domain: String, 7 | } 8 | 9 | 10 | pub fn base_redirect<'x>() -> Structure<'x> { 11 | Structure::new() 12 | .member("redirect_to_domain", Scalar::new()) 13 | } 14 | -------------------------------------------------------------------------------- /src/config/replication.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use quire::validate::{Structure, Sequence, Scalar, Numeric}; 3 | 4 | use crate::config::listen::{self, Listen}; 5 | 6 | 7 | #[derive(Debug, Deserialize, PartialEq, Eq)] 8 | pub struct Replication { 9 | pub listen: Listen, 10 | pub peers: Vec, 11 | pub max_connections: usize, 12 | #[serde(with="::quire::duration")] 13 | pub listen_error_timeout: Duration, 14 | #[serde(with="::quire::duration")] 15 | pub reconnect_timeout: Duration, 16 | } 17 | 18 | pub fn validator<'x>() -> Structure<'x> { 19 | Structure::new() 20 | .member("listen", Sequence::new(listen::validator())) 21 | .member("peers", Sequence::new(listen::validator())) 22 | .member("max_connections", 23 | Numeric::new().min(1).max(1 << 31).default(10)) 24 | .member("listen_error_timeout", Scalar::new().default("100ms")) 25 | .member("reconnect_timeout", Scalar::new().default("5s")) 26 | } 27 | -------------------------------------------------------------------------------- /src/config/self_status.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use quire::validate::{Structure, Mapping, Scalar}; 4 | use crate::config::static_files::header_contains; 5 | use serde::de::{Deserialize, Deserializer}; 6 | 7 | 8 | #[derive(Debug, PartialEq, Eq)] 9 | pub struct SelfStatus { 10 | pub extra_headers: HashMap, 11 | // Computed values 12 | pub overrides_content_type: bool, 13 | } 14 | 15 | pub fn validator<'x>() -> Structure<'x> { 16 | Structure::new() 17 | .member("extra_headers", Mapping::new(Scalar::new(), Scalar::new())) 18 | } 19 | 20 | impl<'a> Deserialize<'a> for SelfStatus { 21 | fn deserialize>(d: D) -> Result { 22 | #[derive(Deserialize)] 23 | pub struct Internal { 24 | pub extra_headers: HashMap, 25 | } 26 | let int = Internal::deserialize(d)?; 27 | return Ok(SelfStatus { 28 | overrides_content_type: 29 | header_contains(&int.extra_headers, "Content-Type"), 30 | extra_headers: int.extra_headers, 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/config/session_pools.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use quire::validate::{Structure, Sequence, Scalar, Numeric}; 3 | 4 | use super::listen::{self, Listen}; 5 | use super::http; 6 | 7 | #[derive(Deserialize, Debug, PartialEq, Eq, Clone)] 8 | pub struct SessionPool { 9 | pub listen: Listen, 10 | pub max_connections: usize, 11 | pub pipeline_depth: usize, 12 | #[serde(with="::quire::duration")] 13 | pub listen_error_timeout: Duration, 14 | pub max_payload_size: usize, 15 | pub inactivity_handlers: Vec, 16 | #[serde(with="::quire::duration")] 17 | pub new_connection_idle_timeout: Duration, 18 | #[serde(with="::quire::duration")] 19 | pub client_min_idle_timeout: Duration, 20 | #[serde(with="::quire::duration")] 21 | pub client_max_idle_timeout: Duration, 22 | // XXX: never used 23 | #[serde(with="::quire::duration")] 24 | pub client_default_idle_timeout: Duration, 25 | #[serde(skip)] 26 | pub use_tangle_prefix: Option, 27 | #[serde(skip)] 28 | pub use_tangle_auth: Option, 29 | #[serde(skip)] 30 | pub weak_content_type: Option, 31 | } 32 | 33 | 34 | pub fn validator<'x>() -> Structure<'x> { 35 | Structure::new() 36 | .member("listen", Sequence::new(listen::validator())) 37 | .member("pipeline_depth", 38 | Numeric::new().min(1).max(10000).default(2)) 39 | .member("max_connections", 40 | Numeric::new().min(1).max(1 << 31).default(1000)) 41 | .member("listen_error_timeout", Scalar::new().default("100ms")) 42 | .member("max_payload_size", 43 | Numeric::new().min(1).max(1 << 31).default(10_485_760)) 44 | .member("inactivity_handlers", 45 | Sequence::new(http::destination_validator())) 46 | .member("new_connection_idle_timeout", 47 | Scalar::new().min_length(1).default("60s")) 48 | .member("client_min_idle_timeout", 49 | Scalar::new().min_length(1).default("1s")) 50 | .member("client_max_idle_timeout", 51 | Scalar::new().min_length(1).default("2h")) 52 | .member("client_default_idle_timeout", 53 | Scalar::new().min_length(1).default("1s")) 54 | } 55 | -------------------------------------------------------------------------------- /src/config/visitors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::marker::PhantomData; 3 | use std::str::FromStr; 4 | 5 | use serde::de; 6 | 7 | 8 | pub struct FromStrVisitor(&'static str, PhantomData) 9 | where T: FromStr, T::Err: fmt::Display; 10 | 11 | impl FromStrVisitor 12 | where T::Err: fmt::Display 13 | { 14 | pub fn new(expected: &'static str) -> FromStrVisitor { 15 | FromStrVisitor(expected, PhantomData) 16 | } 17 | } 18 | 19 | impl<'a, T: FromStr> de::Visitor<'a> for FromStrVisitor 20 | where T::Err: fmt::Display 21 | { 22 | type Value = T; 23 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 24 | formatter.write_str(self.0) 25 | } 26 | fn visit_str(self, s: &str) -> Result { 27 | s.parse().map_err(|e| E::custom(e)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/default_error_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ status.code }} {{ status.reason }} 5 | 6 | 7 |

{{ status.code }} {{ status.reason }}

8 |
9 |

Yours faithfully,
10 | swindon web server 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /src/default_error_page.rs: -------------------------------------------------------------------------------- 1 | use tk_http::{Status}; 2 | use tk_http::server::{Error, EncoderDone}; 3 | use trimmer::{Template, Context, Variable, Var, DataError}; 4 | 5 | use crate::template; 6 | use futures::future::{ok, FutureResult}; 7 | use crate::incoming::{reply, Request, Encoder, IntoContext}; 8 | 9 | #[derive(Debug)] 10 | pub struct StatusVar(Status); 11 | 12 | 13 | lazy_static! { 14 | static ref TEMPLATE: Template = template::PARSER.parse( 15 | include_str!("default_error_page.html")) 16 | .expect("default error page is a valid template"); 17 | } 18 | 19 | pub fn serve_error_page(status: Status, ctx: C) 20 | -> Request 21 | { 22 | reply(ctx, move |e| Box::new(error_page(status, e))) 23 | } 24 | 25 | pub fn error_page(status: Status, mut e: Encoder) 26 | -> FutureResult, Error> 27 | { 28 | e.status(status); 29 | if status.response_has_body() { 30 | let status_var = StatusVar(status); 31 | let mut ctx = Context::new(); 32 | ctx.set("status", &status_var); 33 | let body = match TEMPLATE.render(&ctx) { 34 | Ok(body) => body, 35 | Err(e) => { 36 | error!("Error rendering error page for {:?}: {}", status, e); 37 | "Error rendering error page".into() 38 | } 39 | }; 40 | e.add_length(body.as_bytes().len() as u64); 41 | e.add_header("Content-Type", "text/html"); 42 | if e.done_headers() { 43 | e.write_body(body); 44 | } 45 | } else { 46 | e.done_headers(); 47 | } 48 | ok(e.done()) 49 | } 50 | 51 | impl<'a> Variable<'a> for StatusVar { 52 | fn typename(&self) -> &'static str { 53 | "Status" 54 | } 55 | fn attr<'x>(&'x self, attr: &str) 56 | -> Result, DataError> 57 | where 'a: 'x 58 | { 59 | match attr { 60 | "code" => Ok(Var::owned(self.0.code())), 61 | "reason" => Ok(Var::str(self.0.reason())), 62 | _ => Err(DataError::AttrNotFound), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swindon-rs/swindon/a4b912d678b4159624b53870b1670134fbc32d91/src/empty.gif -------------------------------------------------------------------------------- /src/handlers/empty_gif.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tk_http::Status; 4 | use futures::future::{ok}; 5 | 6 | use crate::config::empty_gif::EmptyGif; 7 | use crate::incoming::{reply, Request, Input}; 8 | 9 | 10 | const EMPTY_GIF: &'static [u8] = include_bytes!("../empty.gif"); 11 | 12 | 13 | pub fn serve(settings: &Arc, inp: Input) 14 | -> Request 15 | { 16 | let settings = settings.clone(); 17 | reply(inp, move |mut e| { 18 | e.status(Status::Ok); 19 | e.add_length(EMPTY_GIF.len() as u64); 20 | if !settings.overrides_content_type { 21 | e.add_header("Content-Type", "image/gif"); 22 | } 23 | e.add_extra_headers(&settings.extra_headers); 24 | if e.done_headers() { 25 | e.write_body(EMPTY_GIF); 26 | } 27 | Box::new(ok(e.done())) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/handlers/files/decode.rs: -------------------------------------------------------------------------------- 1 | pub fn decode_component(buf: &mut Vec, component: &str) -> Result<(), ()> 2 | { 3 | let mut chariter = component.as_bytes().iter(); 4 | while let Some(c) = chariter.next() { 5 | match *c { 6 | b'%' => { 7 | let h = from_hex(*chariter.next().ok_or(())?)?; 8 | let l = from_hex(*chariter.next().ok_or(())?)?; 9 | let b = (h << 4) | l; 10 | if b == 0 || b == b'/' { 11 | return Err(()); 12 | } 13 | buf.push(b); 14 | } 15 | 0 => return Err(()), 16 | c => buf.push(c), 17 | } 18 | } 19 | Ok(()) 20 | } 21 | 22 | fn from_hex(b: u8) -> Result { 23 | match b { 24 | b'0'..=b'9' => Ok(b & 0x0f), 25 | b'a'..=b'f' | b'A'..=b'F' => Ok((b & 0x0f) + 9), 26 | _ => Err(()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/handlers/files/default_dir_index.html: -------------------------------------------------------------------------------- 1 | ## syntax: indent 2 | 3 | 4 | 5 | Listing of the directory {{ path }} 6 | 7 | 8 |

Listing of the directory {{ path }}

9 |
    10 | ## for entry in entries 11 |
  • 12 | ## if entry.is_dir 13 | {{ entry.name }}/ 14 | ## else 15 | {{ entry.name }} 16 | ## endif 17 | {# TODO(tailhook) add some file attributes #} 18 |
  • 19 | ## endfor 20 |
21 |
22 |

Yours faithfully,
23 | swindon web server 24 |

25 | 26 | 27 | -------------------------------------------------------------------------------- /src/handlers/files/mod.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod decode; 3 | mod index; 4 | mod pools; 5 | 6 | mod normal; 7 | mod single; 8 | mod versioned; 9 | 10 | pub use self::pools::DiskPools; 11 | pub use self::single::serve_file; 12 | pub use self::normal::serve_dir; 13 | pub use self::versioned::serve_versioned; 14 | -------------------------------------------------------------------------------- /src/handlers/files/single.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::sync::{Arc}; 3 | 4 | use tk_http::Status; 5 | use http_file_headers::{Input as HeadersInput}; 6 | 7 | use crate::config::static_files::{SingleFile}; 8 | use crate::default_error_page::{serve_error_page}; 9 | use crate::incoming::{Input, Request, Transport}; 10 | use crate::handlers::files::pools::get_pool; 11 | use crate::handlers::files::common::{reply_file, NotFile}; 12 | 13 | 14 | pub fn serve_file(settings: &Arc, mut inp: Input) 15 | -> Request 16 | { 17 | if !inp.headers.path().is_some() { 18 | // Star or authority 19 | return serve_error_page(Status::Forbidden, inp); 20 | }; 21 | inp.debug.set_fs_path(&settings.path); 22 | let pool = get_pool(&inp.runtime, &settings.pool); 23 | let settings = settings.clone(); 24 | let settings2 = settings.clone(); 25 | 26 | let hinp = HeadersInput::from_headers(&settings.headers_config, 27 | inp.headers.method(), inp.headers.headers()); 28 | let fut = pool.spawn_fn(move || { 29 | hinp.probe_file(&settings2.path) 30 | .map(|x| (x, ())) 31 | .map_err(|e| { 32 | if e.kind() == io::ErrorKind::PermissionDenied { 33 | (NotFile::Status(Status::Forbidden), ()) 34 | } else { 35 | error!("Error reading file {:?}: {}", settings2.path, e); 36 | (NotFile::Status(Status::InternalServerError), ()) 37 | } 38 | }) 39 | }); 40 | 41 | reply_file(inp, pool, fut, move |e, ()| { 42 | if let Some(ref val) = settings.content_type { 43 | e.add_header("Content-Type", val); 44 | } 45 | e.add_extra_headers(&settings.extra_headers); 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod empty_gif; 2 | pub mod files; 3 | pub mod websocket_echo; 4 | pub mod swindon_chat; 5 | pub mod proxy; 6 | pub mod redirect; 7 | pub mod self_status; 8 | -------------------------------------------------------------------------------- /src/handlers/proxy.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use crate::proxy::frontend::Codec; 3 | 4 | use tk_http::Status; 5 | use tk_http::server::RequestTarget::Authority; 6 | use crate::config::proxy::Proxy; 7 | use crate::incoming::{Request, Input}; 8 | use crate::default_error_page::serve_error_page; 9 | 10 | 11 | pub fn serve(settings: &Arc, inp: Input) 12 | -> Request 13 | { 14 | if inp.headers.host().is_none() { 15 | // Can't proxy without Host 16 | return serve_error_page(Status::BadRequest, inp) 17 | } 18 | if matches!(*inp.headers.request_target(), Authority(..)) { 19 | // Can't proxy without Host 20 | return serve_error_page(Status::BadRequest, inp) 21 | } 22 | Box::new(Codec::new(settings, inp)) 23 | } 24 | -------------------------------------------------------------------------------- /src/handlers/redirect.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tk_http::Status; 4 | use futures::future::ok; 5 | 6 | use crate::default_error_page::serve_error_page; 7 | use crate::config::redirect::BaseRedirect; 8 | use crate::incoming::{reply, Request, Input}; 9 | 10 | 11 | pub fn base_redirect(settings: &Arc, inp: Input) 12 | -> Request 13 | { 14 | serve_redirect(settings.redirect_to_domain.as_str(), 15 | Status::MovedPermanently, inp) 16 | } 17 | 18 | 19 | pub fn strip_www_redirect(inp: Input) 20 | -> Request 21 | { 22 | 23 | let base_host = inp.headers.host().and_then(|h| { 24 | if h.len() > 4 && h[0..4].eq_ignore_ascii_case("www.") { 25 | Some(&h[4..]) 26 | } else { 27 | None 28 | } 29 | }); 30 | match base_host { 31 | Some(host) => serve_redirect(host, Status::MovedPermanently, inp), 32 | None => serve_error_page(Status::NotFound, inp), 33 | } 34 | } 35 | 36 | 37 | fn serve_redirect(host: &str, status: Status, inp: Input) 38 | -> Request 39 | { 40 | // TODO: properly identify request scheme 41 | let dest = format!("http://{}{}", host, inp.headers.path().unwrap_or("/")); 42 | reply(inp, move |mut e| { 43 | e.status(status); 44 | e.add_header("Location", dest); 45 | e.add_length(0); 46 | if e.done_headers() { 47 | // TODO: add HTML with redirect link; 48 | // link must be url-encoded; 49 | } 50 | Box::new(ok(e.done())) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/handlers/self_status.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufWriter; 2 | use std::sync::Arc; 3 | 4 | use futures::future::{ok}; 5 | use libcantal::{Json, Collection}; 6 | use self_meter_http::{ThreadReport, ProcessReport}; 7 | use serde_json; 8 | use tk_http::Status; 9 | 10 | use crate::config::self_status::SelfStatus; 11 | use crate::incoming::{reply, Request, Input}; 12 | use crate::metrics; 13 | 14 | 15 | pub fn serve(settings: &Arc, inp: Input) 16 | -> Request 17 | { 18 | let settings = settings.clone(); 19 | let meter = inp.runtime.meter.clone(); 20 | let fingerprint = inp.runtime.config.fingerprint(); 21 | let runtime = inp.runtime.clone(); 22 | 23 | reply(inp, move |mut e| { 24 | 25 | #[derive(Serialize)] 26 | struct Response<'a> { 27 | process: ProcessReport<'a>, 28 | threads: ThreadReport<'a>, 29 | metrics: Json<'a, Vec>>, 30 | config_fingerprint: String, 31 | version: &'a str, 32 | } 33 | 34 | e.status(Status::Ok); 35 | e.add_chunked(); 36 | if !settings.overrides_content_type { 37 | e.add_header("Content-Type", "application/json"); 38 | } 39 | e.add_extra_headers(&settings.extra_headers); 40 | if e.done_headers() { 41 | serde_json::to_writer(BufWriter::new(&mut e), &Response { 42 | process: meter.process_report(), 43 | threads: meter.thread_report(), 44 | metrics: Json(&metrics::all(&runtime)), 45 | config_fingerprint: fingerprint, 46 | version: env!("CARGO_PKG_VERSION"), 47 | }).expect("report is serializable"); 48 | } 49 | Box::new(ok(e.done())) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/handlers/websocket_echo.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::{Async, Future}; 4 | use futures::stream::{Stream}; 5 | use tk_http::Status; 6 | use tk_http::server::{Error, Codec, RecvMode}; 7 | use tk_http::server as http; 8 | use tk_http::websocket::{ServerCodec as WebsocketCodec, Accept}; 9 | use tk_bufstream::{ReadBuf, WriteBuf}; 10 | use futures::future::{ok}; 11 | use tokio_core::reactor::Handle; 12 | use tokio_io::{AsyncRead, AsyncWrite}; 13 | 14 | use crate::config::Config; 15 | use crate::incoming::{Request, Input, Debug, Reply, Encoder}; 16 | use crate::default_error_page::serve_error_page; 17 | 18 | 19 | struct WebsockReply { 20 | rdata: Option<(Arc, Debug, Accept)>, 21 | handle: Handle, 22 | } 23 | 24 | 25 | impl Codec for WebsockReply { 26 | type ResponseFuture = Reply; 27 | fn recv_mode(&mut self) -> RecvMode { 28 | RecvMode::hijack() 29 | } 30 | fn data_received(&mut self, data: &[u8], end: bool) 31 | -> Result, Error> 32 | { 33 | assert!(end); 34 | assert!(data.len() == 0); 35 | Ok(Async::Ready(0)) 36 | } 37 | fn start_response(&mut self, e: http::Encoder) -> Reply { 38 | let (config, debug, accept) = self.rdata.take() 39 | .expect("start response called once"); 40 | let mut e = Encoder::new(e, (config, debug)); 41 | e.status(Status::SwitchingProtocol); 42 | e.add_header("Connection", "upgrade"); 43 | e.add_header("Upgrade", "websocket"); 44 | e.format_header("Sec-Websocket-Accept", &accept); 45 | e.done_headers(); 46 | Box::new(ok(e.done())) 47 | } 48 | fn hijack(&mut self, write_buf: WriteBuf, read_buf: ReadBuf) { 49 | let inp = read_buf.framed(WebsocketCodec); 50 | let out = write_buf.framed(WebsocketCodec); 51 | // TODO(tailhook) convert Ping to Pong (and Close ?) before echoing 52 | self.handle.spawn(inp.forward(out) 53 | .map(|_| ()) 54 | // TODO(tailhook) check error reporting 55 | .map_err(|e| info!("Websocket error: {}", e))) 56 | } 57 | } 58 | 59 | pub fn serve(inp: Input) -> Request { 60 | match inp.headers.get_websocket_upgrade() { 61 | Ok(Some(ws)) => { 62 | Box::new(WebsockReply { 63 | rdata: Some((inp.config.clone(), inp.debug, ws.accept)), 64 | handle: inp.handle.clone(), 65 | }) 66 | } 67 | Ok(None) => { 68 | serve_error_page(Status::NotFound, inp) 69 | } 70 | Err(()) => { 71 | serve_error_page(Status::BadRequest, inp) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/incoming/authorizer.rs: -------------------------------------------------------------------------------- 1 | use tk_http::server::{Error}; 2 | 3 | use crate::incoming::{Input}; 4 | use crate::config::{Authorizer}; 5 | use crate::authorizers; 6 | 7 | // TODO(tailhook) this should eventually be a virtual method on Authorizer 8 | impl Authorizer { 9 | pub fn check(&self, input: &mut Input) -> Result { 10 | match *self { 11 | Authorizer::AllowAll => Ok(true), 12 | Authorizer::SourceIp(ref cfg) => { 13 | authorizers::source_ip::check(cfg, input) 14 | } 15 | Authorizer::Ldap(_) => unimplemented!(), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/incoming/handler.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use tk_http::server::{Dispatcher, Error}; 4 | use tk_http::Status; 5 | use httpbin::HttpBin; 6 | 7 | use crate::config::{Handler}; 8 | use crate::handlers; 9 | use crate::incoming::{Request, Input, Transport}; 10 | use crate::default_error_page::serve_error_page; 11 | 12 | 13 | // TODO(tailhook) this should eventually be a virtual method on Handler trait 14 | impl Handler { 15 | pub fn serve(&self, input: Input) -> Result, Error> 16 | where S: Transport 17 | { 18 | match *self { 19 | Handler::EmptyGif(ref h) => { 20 | Ok(handlers::empty_gif::serve(h, input)) 21 | } 22 | Handler::NotFound => { 23 | Ok(serve_error_page(Status::NotFound, input)) 24 | } 25 | Handler::HttpBin => { 26 | HttpBin::new_at(&Path::new( 27 | if input.prefix == "" { "/" } else { input.prefix })) 28 | .instantiate(input.addr) 29 | .headers_received(input.headers) 30 | } 31 | Handler::Static(ref settings) => { 32 | Ok(handlers::files::serve_dir(settings, input)) 33 | } 34 | Handler::SingleFile(ref settings) => { 35 | Ok(handlers::files::serve_file(settings, input)) 36 | } 37 | Handler::VersionedStatic(ref settings) => { 38 | Ok(handlers::files::serve_versioned(settings, input)) 39 | } 40 | Handler::WebsocketEcho => { 41 | Ok(handlers::websocket_echo::serve(input)) 42 | } 43 | Handler::Proxy(ref settings) => { 44 | Ok(handlers::proxy::serve(settings, input)) 45 | } 46 | Handler::SwindonLattice(ref settings) => { 47 | handlers::swindon_chat::serve(settings, input) 48 | } 49 | Handler::BaseRedirect(ref settings) => { 50 | Ok(handlers::redirect::base_redirect(settings, input)) 51 | } 52 | Handler::StripWWWRedirect => { 53 | Ok(handlers::redirect::strip_www_redirect(input)) 54 | } 55 | Handler::SelfStatus(ref settings) => { 56 | Ok(handlers::self_status::serve(settings, input)) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/incoming/input.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::net::SocketAddr; 3 | 4 | use tk_http::server::Head; 5 | use tokio_core::reactor::Handle; 6 | 7 | use crate::config::Config; 8 | use crate::runtime::Runtime; 9 | use crate::incoming::{Debug, IntoContext}; 10 | use crate::request_id::RequestId; 11 | 12 | 13 | pub struct Input<'a> { 14 | pub addr: SocketAddr, 15 | pub runtime: &'a Arc, 16 | pub config: &'a Arc, 17 | pub debug: Debug, 18 | pub headers: &'a Head<'a>, 19 | pub prefix: &'a str, 20 | pub suffix: &'a str, 21 | pub handle: &'a Handle, 22 | pub request_id: RequestId, 23 | } 24 | 25 | impl<'a> IntoContext for Input<'a> { 26 | fn into_context(self) -> (Arc, Debug) { 27 | (self.config.clone(), self.debug) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/incoming/mod.rs: -------------------------------------------------------------------------------- 1 | use futures::future::Future; 2 | use tk_http::server::{Codec, Error}; 3 | use tokio_io::{AsyncRead, AsyncWrite}; 4 | 5 | use crate::metrics::{Metric, List}; 6 | 7 | mod input; 8 | mod router; 9 | mod debug; 10 | mod encoder; 11 | mod quick_reply; 12 | mod handler; 13 | mod authorizer; 14 | 15 | pub type Request = Box>>; 16 | pub type Reply = Box, Error=Error>>; 17 | 18 | pub use self::debug::Debug; 19 | pub use tk_http::server::EncoderDone; 20 | pub use self::encoder::{Encoder, IntoContext, Context}; 21 | pub use self::input::{Input}; 22 | pub use self::quick_reply::reply; 23 | pub use self::router::Router; 24 | 25 | /// A transport trait. We currently include ``AsRawFd`` in it to allow 26 | /// sendfile to work. But in the future we want to use specialization 27 | /// to optimize sendfile 28 | pub trait Transport: AsyncRead + AsyncWrite + Send + 'static {} 29 | impl Transport for T {} 30 | 31 | pub fn metrics() -> List { 32 | vec![ 33 | // obeys cantal-py.RequestTracker 34 | (Metric("frontend.incoming", "requests"), &*router::REQUESTS), 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/incoming/quick_reply.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::Async; 4 | use tk_http::server::{Error, Codec, RecvMode}; 5 | use tk_http::server as http; 6 | 7 | use crate::config::Config; 8 | use crate::incoming::{Request, Reply, Encoder, IntoContext, Debug}; 9 | 10 | 11 | pub struct QuickReply { 12 | inner: Option<(F, Arc, Debug)>, 13 | } 14 | 15 | 16 | pub fn reply(ctx: C, f: F) 17 | -> Request 18 | where F: FnOnce(Encoder) -> Reply + 'static, 19 | C: IntoContext, 20 | { 21 | let (cfg, debug) = ctx.into_context(); 22 | Box::new(QuickReply { 23 | inner: Some((f, cfg, debug)), 24 | }) 25 | } 26 | 27 | impl Codec for QuickReply 28 | where F: FnOnce(Encoder) -> Reply, 29 | { 30 | type ResponseFuture = Reply; 31 | fn recv_mode(&mut self) -> RecvMode { 32 | RecvMode::buffered_upfront(0) 33 | } 34 | fn data_received(&mut self, data: &[u8], end: bool) 35 | -> Result, Error> 36 | { 37 | assert!(end); 38 | assert!(data.len() == 0); 39 | Ok(Async::Ready(0)) 40 | } 41 | fn start_response(&mut self, e: http::Encoder) -> Reply { 42 | let (func, config, debug) = self.inner.take() 43 | .expect("start response called once"); 44 | func(Encoder::new(e, (config, debug))) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/logging/context.rs: -------------------------------------------------------------------------------- 1 | pub use trimmer::Context; 2 | 3 | pub trait AsContext { 4 | fn as_context(&self) -> Context; 5 | } 6 | -------------------------------------------------------------------------------- /src/logging/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pub fn early_error(rt: &Arc, status: Status, debug: &Debug) { 5 | let cfg = rt.config.get(); 6 | if cfg.debug_logging { 7 | if let Some(ref fmt) = cfg.log_formats.get("debug-log") { 8 | EarlyContext { 9 | request: EarlyRequest { 10 | }, 11 | request: EarlyResponse { 12 | status: status, 13 | }, 14 | }.log(fmt, BufWriter::new(stdout())); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/logging/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | mod context; 3 | pub mod http; 4 | 5 | pub use self::context::AsContext; 6 | 7 | 8 | use std::io::{stdout, Write}; 9 | use std::sync::Arc; 10 | 11 | use crate::runtime::Runtime; 12 | 13 | 14 | pub fn log(runtime: &Arc, ctx: C) { 15 | let cfg = runtime.config.get(); 16 | if cfg.debug_logging { 17 | if let Some(ref fmt) = cfg.log_formats.get("debug-log") { 18 | let ctx = ctx.as_context(); 19 | match fmt.template.render(&ctx) { 20 | Ok(mut line) => { 21 | line.push('\n'); 22 | stdout().write_all(line.as_bytes()) 23 | .map_err(|e| { 24 | warn!("Can't write debug log: {}", e) 25 | }).ok(); 26 | } 27 | Err(e) => { 28 | warn!("Can't log request: {:?}", e); 29 | } 30 | }; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use libcantal::{self, Name, NameVisitor, Value, Collection, Error}; 4 | use owning_ref::OwningHandle; 5 | 6 | use crate::runtime::Runtime; 7 | 8 | pub use libcantal::{Counter, Integer}; 9 | 10 | pub type List = Vec<(Metric<'static>, &'static dyn Value)>; 11 | 12 | pub struct Metric<'a>(pub &'a str, pub &'a str); 13 | 14 | // this is not actually static, but we have no lifetime name for it 15 | struct Wrapper(libcantal::ActiveCollection<'static>); 16 | 17 | pub struct ActiveCollection(OwningHandle>>, Wrapper>); 18 | 19 | impl<'a> Name for Metric<'a> { 20 | fn get(&self, key: &str) -> Option<&str> { 21 | match key { 22 | "metric" => Some(self.1), 23 | "group" => Some(self.0), 24 | _ => None, 25 | } 26 | } 27 | fn visit(&self, s: &mut dyn NameVisitor) { 28 | s.visit_pair("group", self.0); 29 | s.visit_pair("metric", self.1); 30 | } 31 | } 32 | 33 | impl ::std::ops::Deref for Wrapper { 34 | type Target = (); 35 | fn deref(&self) -> &() { &() } 36 | } 37 | 38 | pub fn all(runtime: &Arc) -> Box>> { 39 | Box::new(vec![ 40 | Box::new(crate::incoming::metrics()), 41 | Box::new(crate::chat::metrics()), 42 | Box::new(crate::http_pools::metrics()), 43 | Box::new(crate::http_pools::pool_metrics(&runtime.http_pools)), 44 | ]) 45 | } 46 | 47 | pub fn start(runtime: &Arc) -> Result { 48 | OwningHandle::try_new(all(runtime), |m| { 49 | libcantal::start(unsafe { &*m }).map(Wrapper) 50 | }).map(ActiveCollection) 51 | } 52 | -------------------------------------------------------------------------------- /src/privileges.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use crate::config::Config; 3 | 4 | 5 | #[cfg(unix)] 6 | pub fn drop(cfg: &Config) -> Result<(), io::Error> { 7 | use libc::{getpwnam, getgrnam, setuid, setgid}; 8 | use std::ffi::CString; 9 | use std::ptr; 10 | 11 | if let Some(ref group) = cfg.set_group { 12 | unsafe { 13 | let grstring = CString::new(group.as_bytes()).unwrap(); 14 | let gentry = getgrnam(grstring.as_ptr()); 15 | if gentry == ptr::null_mut() { 16 | return Err(io::Error::last_os_error()); 17 | } 18 | info!("Group {:?} has gid of {}", group, (*gentry).gr_gid); 19 | if setgid((*gentry).gr_gid) == -1 { 20 | return Err(io::Error::last_os_error()); 21 | } 22 | } 23 | } 24 | 25 | if let Some(ref user) = cfg.set_user { 26 | unsafe { 27 | let ustring = CString::new(user.as_bytes()).unwrap(); 28 | let uentry = getpwnam(ustring.as_ptr()); 29 | if uentry == ptr::null_mut() { 30 | return Err(io::Error::last_os_error()); 31 | } 32 | if cfg.set_group.is_none() { 33 | info!("User {:?} has uid of {} and primary group {}", 34 | user, (*uentry).pw_uid, (*uentry).pw_gid); 35 | if setgid((*uentry).pw_gid) == -1 { 36 | return Err(io::Error::last_os_error()); 37 | } 38 | } else { 39 | info!("User {:?} has uid of {}", user, (*uentry).pw_uid); 40 | } 41 | if setuid((*uentry).pw_uid) == -1 { 42 | return Err(io::Error::last_os_error()); 43 | } 44 | } 45 | } 46 | Ok(()) 47 | } 48 | #[cfg(not(unix))] 49 | pub fn drop(_: &Config) -> Result<(), io::Error> { 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /src/proxy/backend.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::sync::Arc; 3 | 4 | use futures::Async; 5 | use futures::future::{FutureResult, ok}; 6 | use futures::sync::oneshot; 7 | use tk_http::client as http; 8 | 9 | use crate::config::http_destinations::Destination; 10 | use crate::proxy::{RepReq, HalfResp, Response}; 11 | 12 | enum State { 13 | Init(RepReq), 14 | Wait, 15 | Headers(HalfResp), 16 | #[allow(dead_code)] 17 | Done(Response), 18 | Void, 19 | } 20 | 21 | 22 | pub struct Codec { 23 | state: State, 24 | destination: Arc, 25 | sender: Option>, 26 | } 27 | 28 | impl Codec { 29 | pub fn new(req: RepReq, destination: &Arc, 30 | tx: oneshot::Sender) 31 | -> Codec 32 | { 33 | Codec { 34 | state: State::Init(req), 35 | destination: destination.clone(), 36 | sender: Some(tx), 37 | } 38 | } 39 | } 40 | 41 | impl http::Codec for Codec { 42 | type Future = FutureResult, http::Error>; 43 | 44 | fn start_write(&mut self, e: http::Encoder) -> Self::Future { 45 | if let State::Init(req) = mem::replace(&mut self.state, State::Void) { 46 | self.state = State::Wait; 47 | ok(req.encode(e, &self.destination)) 48 | } else { 49 | panic!("wrong state"); 50 | } 51 | } 52 | fn headers_received(&mut self, headers: &http::Head) 53 | -> Result 54 | { 55 | if let State::Wait = mem::replace(&mut self.state, State::Void) { 56 | self.state = State::Headers(HalfResp::from_headers(headers)); 57 | // TODO(tailhook) limit and streaming 58 | Ok(http::RecvMode::buffered(10_485_760)) 59 | } else { 60 | panic!("wrong state"); 61 | } 62 | } 63 | fn data_received(&mut self, data: &[u8], end: bool) 64 | -> Result, http::Error> 65 | { 66 | // TODO(tailhook) streaming 67 | assert!(end); 68 | match mem::replace(&mut self.state, State::Void) { 69 | State::Headers(hr) => { 70 | let resp = hr.complete(data.to_vec()); 71 | self.sender.take().unwrap().send(resp).ok(); 72 | } 73 | _ => unreachable!(), 74 | } 75 | Ok(Async::Ready(data.len())) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/proxy/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod frontend; 2 | pub mod backend; 3 | mod response; 4 | mod request; 5 | 6 | pub use self::response::{HalfResp, Response}; 7 | pub use self::request::{HalfReq, RepReq}; 8 | -------------------------------------------------------------------------------- /src/proxy/response.rs: -------------------------------------------------------------------------------- 1 | use tk_http::{Status}; 2 | use tk_http::client::Head; 3 | use tk_http::server::{EncoderDone}; 4 | 5 | use crate::incoming::Encoder; 6 | 7 | 8 | #[derive(Debug)] 9 | pub enum RespStatus { 10 | Normal(Status), 11 | Custom(u16, String), 12 | } 13 | 14 | 15 | pub struct HalfResp { 16 | status: RespStatus, 17 | headers: Vec<(String, Vec)>, 18 | } 19 | 20 | pub struct Response { 21 | status: RespStatus, 22 | headers: Vec<(String, Vec)>, 23 | body: Vec, 24 | } 25 | 26 | impl HalfResp { 27 | pub fn from_headers(head: &Head) -> HalfResp { 28 | let status = head.status().map(RespStatus::Normal) 29 | .unwrap_or_else(|| { 30 | let (c, v) = head.raw_status(); 31 | RespStatus::Custom(c, v.to_string()) 32 | }); 33 | HalfResp { 34 | status: status, 35 | headers: head.headers().map(|(k, v)| { 36 | (k.to_string(), v.to_vec()) 37 | }).collect(), 38 | } 39 | } 40 | pub fn complete(self, body: Vec) -> Response { 41 | Response { 42 | status: self.status, 43 | headers: self.headers, 44 | body: body, 45 | } 46 | } 47 | } 48 | 49 | impl Response { 50 | pub fn encode(&self, mut e: Encoder) -> EncoderDone{ 51 | let body = match self.status { 52 | RespStatus::Normal(s) => { 53 | e.status(s); 54 | s.response_has_body() 55 | } 56 | RespStatus::Custom(c, ref r) => { 57 | e.custom_status(c, r); 58 | true 59 | } 60 | }; 61 | for &(ref k, ref v) in &self.headers { 62 | e.add_header(k, v); 63 | } 64 | if body { 65 | e.add_length(self.body.len() as u64); 66 | if e.done_headers() { 67 | e.write_body(&self.body); 68 | } 69 | } else { 70 | let res = e.done_headers(); 71 | assert!(res == false); 72 | } 73 | return e.done(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/runtime.rs: -------------------------------------------------------------------------------- 1 | use tokio_core::reactor::Handle; 2 | 3 | use crate::chat; 4 | use crate::config::ConfigCell; 5 | use crate::handlers::files; 6 | use crate::http_pools::HttpPools; 7 | use self_meter_http::Meter; 8 | use crate::request_id::RequestId; 9 | use ns_router::Router; 10 | 11 | 12 | pub struct Runtime { 13 | pub config: ConfigCell, 14 | pub handle: Handle, 15 | pub http_pools: HttpPools, 16 | pub session_pools: chat::SessionPools, 17 | pub disk_pools: files::DiskPools, 18 | pub meter: Meter, 19 | pub server_id: ServerId, 20 | pub resolver: Router, 21 | } 22 | 23 | /// Runtime server identifier. 24 | /// Used mainly in chat. 25 | pub type ServerId = RequestId; 26 | -------------------------------------------------------------------------------- /src/template.rs: -------------------------------------------------------------------------------- 1 | use trimmer::{Parser}; 2 | 3 | 4 | lazy_static! { 5 | /// This holds parser so we don't need to compile it's comlplex regexes 6 | /// every time 7 | pub static ref PARSER: Parser = Parser::new(); 8 | } 9 | -------------------------------------------------------------------------------- /src/updater/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use async_slot as slot; 5 | 6 | use crate::config::{Configurator}; 7 | 8 | 9 | fn updater(tx: slot::Sender<()>, mut configurator: Configurator) { 10 | loop { 11 | thread::sleep(Duration::new(10, 0)); 12 | match configurator.try_update() { 13 | Ok(false) => {} 14 | Ok(true) => { 15 | tx.swap(()).expect("Can send updated config"); 16 | } 17 | Err(e) => { 18 | error!("Reading new config: {}", e); 19 | } 20 | } 21 | } 22 | } 23 | 24 | pub fn update_thread(configurator: Configurator) -> slot::Receiver<()> { 25 | let (tx, rx) = slot::channel(); 26 | thread::spawn(move || updater(tx, configurator)); 27 | return rx; 28 | } 29 | -------------------------------------------------------------------------------- /tests/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 403 Forbidden 5 | 6 | 7 |

403 Forbidden

8 |
9 |

Yours faithfully,
10 | swindon web server 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 404 Not Found 5 | 6 | 7 |

404 Not Found

8 |
9 |

Yours faithfully,
10 | swindon web server 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 500 Internal Server Error 5 | 6 | 7 |

500 Internal Server Error

8 |
9 |

Yours faithfully,
10 | swindon web server 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/README.rst: -------------------------------------------------------------------------------- 1 | Functional py.test tests for swindon. 2 | 3 | Run with:: 4 | 5 | $ vagga func-test 6 | -------------------------------------------------------------------------------- /tests/assets/a+b.txt: -------------------------------------------------------------------------------- 1 | a+b 2 | -------------------------------------------------------------------------------- /tests/assets/index/index.html: -------------------------------------------------------------------------------- 1 | 2 | Hello 3 | -------------------------------------------------------------------------------- /tests/assets/link.txt: -------------------------------------------------------------------------------- 1 | static_file.txt -------------------------------------------------------------------------------- /tests/assets/localhost/static-w-hostname/test.txt: -------------------------------------------------------------------------------- 1 | localhost+static 2 | -------------------------------------------------------------------------------- /tests/assets/static_file.html: -------------------------------------------------------------------------------- 1 | Static file test 2 | -------------------------------------------------------------------------------- /tests/assets/static_file.txt: -------------------------------------------------------------------------------- 1 | Static file test 2 | -------------------------------------------------------------------------------- /tests/assets/test.html: -------------------------------------------------------------------------------- 1 | 2 | file-from-assets 3 | -------------------------------------------------------------------------------- /tests/auth_test.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | def assert_gif(resp, data, debug_routing): 4 | assert resp.status == 200 5 | assert resp.headers['Content-Type'] == 'image/gif' 6 | assert resp.headers['Content-Length'] == '26' 7 | assert resp.headers['Server'] == 'swindon/func-tests' 8 | if debug_routing: 9 | assert resp.headers['X-Swindon-Route'] == 'empty_gif' 10 | assert len(data) == 26 11 | 12 | def assert_403(resp, data, debug_routing): 13 | assert resp.status == 403 14 | 15 | 16 | async def test_local_ok(swindon, http_request, debug_routing): 17 | resp, data = await http_request(swindon.url / 'auth/local') 18 | assert_gif(resp, data, debug_routing) 19 | if debug_routing: 20 | assert resp.headers['X-Swindon-Authorizer'] == 'only-127-0-0-1' 21 | assert resp.headers['X-Swindon-Allow'] == 'source-ip 127.0.0.1/24' 22 | 23 | 24 | async def test_forwarded_ok(swindon, http_request, debug_routing): 25 | resp, data = await http_request(swindon.url / 'auth/by-header', 26 | headers={"X-Real-Ip": "8.8.8.8"}) 27 | if debug_routing: 28 | assert resp.headers['X-Swindon-Authorizer'] == 'by-header' 29 | assert resp.headers['X-Swindon-Allow'] == \ 30 | 'forwarded-from 127.0.0.1/24, source-ip 8.0.0.0/8' 31 | 32 | 33 | async def test_forwarded_bad(swindon, http_request, debug_routing): 34 | resp, data = await http_request(swindon.url / 'auth/by-header', 35 | headers={"X-Real-Ip": "4.4.4.4"}) 36 | assert_403(resp, data, debug_routing) 37 | if debug_routing: 38 | assert resp.headers['X-Swindon-Authorizer'] == 'by-header' 39 | assert resp.headers['X-Swindon-Deny'] == 'source-ip 4.4.4.4' 40 | -------------------------------------------------------------------------------- /tests/base_redirect_test.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | 4 | async def test_ok(swindon, proxy_request_method, http_version, 5 | debug_routing, loop): 6 | url = 'http://example.com:{}/empty.gif'.format(swindon.url.port) 7 | kw = {"allow_redirects": False} 8 | 9 | async with aiohttp.ClientSession(version=http_version, loop=loop) as s: 10 | async with s.request(proxy_request_method, url, **kw) as resp: 11 | assert resp.status == 301 12 | assert resp.headers.getall("Location") == [ 13 | "http://localhost/empty.gif" 14 | ] 15 | if debug_routing: 16 | assert 'X-Swindon-Route' in resp.headers 17 | else: 18 | assert 'X-Swindon-Route' not in resp.headers 19 | assert await resp.read() == b'' 20 | -------------------------------------------------------------------------------- /tests/empty_gif_test.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | 4 | async def test_ok(swindon, http_request, debug_routing): 5 | resp, data = await http_request(swindon.url / 'empty.gif') 6 | assert resp.status == 200 7 | assert resp.headers['Content-Type'] == 'image/gif' 8 | assert resp.headers['Content-Length'] == '26' 9 | assert resp.headers['Server'] == 'swindon/func-tests' 10 | if debug_routing: 11 | assert resp.headers['X-Swindon-Route'] == 'empty_gif' 12 | assert len(data) == 26 13 | 14 | 15 | async def test_request_methods(swindon, http_request): 16 | resp, data = await http_request(swindon.url / 'empty.gif') 17 | assert resp.status == 200 18 | assert resp.headers['Content-Type'] == 'image/gif' 19 | assert resp.headers['Content-Length'] == '26' 20 | assert resp.headers['Server'] == 'swindon/func-tests' 21 | assert len(data) == 26 22 | 23 | 24 | async def test_request_HEAD(swindon, loop): 25 | async with aiohttp.ClientSession(loop=loop) as s: 26 | async with s.head(swindon.url / 'empty.gif') as resp: 27 | assert resp.status == 200 28 | assert resp.headers['Content-Type'] == 'image/gif' 29 | assert resp.headers['Content-Length'] == '26' 30 | assert resp.headers['Server'] == 'swindon/func-tests' 31 | data = await resp.content.read() 32 | assert len(data) == 0 33 | 34 | 35 | async def test_extra_headers(swindon, http_request): 36 | resp, data = await http_request(swindon.url / 'empty-w-headers.gif') 37 | assert resp.status == 200 38 | assert resp.headers['X-Some-Header'] == 'some value' 39 | 40 | 41 | async def test_headers_override(swindon, http_request): 42 | url = swindon.url / 'empty-w-content-length.gif' 43 | resp, data = await http_request(url) 44 | assert resp.status == 200 45 | clen = [val for key, val in resp.raw_headers 46 | if key == b'Content-Length'] 47 | assert len(clen) == 1 48 | assert resp.headers['Content-Length'] == '26' 49 | 50 | ctype = [val for key, val in resp.raw_headers 51 | if key == b'Content-Type'] 52 | assert len(ctype) == 1 53 | assert ctype[0] == b'image/other' 54 | -------------------------------------------------------------------------------- /tests/hashed/aa/bbbbbb-a+b.txt: -------------------------------------------------------------------------------- 1 | a+b at aabbbbbb 2 | -------------------------------------------------------------------------------- /tests/hashed/aa/bbbbbb-test.html: -------------------------------------------------------------------------------- 1 | 2 | Hello 3 | -------------------------------------------------------------------------------- /tests/hashed/bb/aaaaaa-test.html: -------------------------------------------------------------------------------- 1 | 2 | Greetings 3 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | wsgi: WSGI proxy backend 4 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | aiohttp==3.6.1 3 | pytest==5.2.0 4 | pytest-aiohttp==0.3.0 5 | pytest-xdist>=1.15.0 6 | uvloop==0.13.0 7 | Werkzeug==0.16.0 8 | -------------------------------------------------------------------------------- /tests/strip_www_redirect_test.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | 4 | async def test_ok(swindon, proxy_request_method, http_version, 5 | debug_routing, loop): 6 | url = 'http://www.example.com:{}/empty.gif'.format(swindon.url.port) 7 | kw = {"allow_redirects": False} 8 | 9 | async with aiohttp.ClientSession(version=http_version, loop=loop) as s: 10 | async with s.request(proxy_request_method, url, **kw) as resp: 11 | assert resp.status == 301 12 | assert resp.headers.getall("Location") == [ 13 | "http://example.com:{}/empty.gif".format(swindon.url.port) 14 | ] 15 | if debug_routing: 16 | assert 'X-Swindon-Route' in resp.headers 17 | else: 18 | assert 'X-Swindon-Route' not in resp.headers 19 | assert await resp.read() == b'' 20 | -------------------------------------------------------------------------------- /tests/swindon_chat_inactivity.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from unittest import mock 4 | 5 | 6 | def assert_auth(req): 7 | assert req.path == '/swindon/authorize_connection' 8 | assert req.headers["Host"] == "swindon.internal" 9 | assert req.headers['Content-Type'] == 'application/json' 10 | assert re.match('^swindon/(\d+\.){2}\d+$', req.headers['User-Agent']) 11 | assert 'Authorization' not in req.headers 12 | 13 | 14 | def assert_headers(req): 15 | assert req.headers["Host"] == "swindon.internal" 16 | assert req.headers['Content-Type'] == 'application/json' 17 | assert re.match('^swindon/(\d+\.){2}\d+$', req.headers['User-Agent']) 18 | 19 | 20 | async def test_inactivity(proxy_server, swindon, loop): 21 | chat_url = swindon.url / 'swindon-lattice-w-timeouts' 22 | async with proxy_server() as proxy: 23 | handler = proxy.swindon_lattice(chat_url, timeout=1) 24 | req = await handler.request() 25 | assert_auth(req) 26 | ws = await handler.json_response({ 27 | "user_id": 'user:1', "username": "Jim"}) 28 | 29 | hello = await ws.receive_json() 30 | assert hello == [ 31 | 'hello', {}, {'user_id': 'user:1', 'username': 'Jim'}] 32 | 33 | req = await handler.request(timeout=1.2) 34 | assert req.path == '/swindon/session_inactive' 35 | assert_headers(req) 36 | assert req.headers.getall('Authorization') == [ 37 | 'Tangle eyJ1c2VyX2lkIjoidXNlcjoxIn0=' 38 | ] 39 | assert await req.json() == [{}, [], {}] 40 | await handler.response(status=204) 41 | 42 | await ws.send_json([ 43 | 'whatever', {'request_id': '1', 'active': 2}, [], {}]) 44 | req = await handler.request(timeout=5) 45 | assert req.path == '/whatever' 46 | assert_headers(req) 47 | assert await req.json() == [ 48 | {'request_id': '1', 'active': 2, 'connection_id': mock.ANY}, 49 | [], {}] 50 | await handler.response(status=200) 51 | 52 | req = await handler.request(timeout=3.2) 53 | assert req.path == '/swindon/session_inactive' 54 | assert_headers(req) 55 | assert req.headers.getall('Authorization') == [ 56 | 'Tangle eyJ1c2VyX2lkIjoidXNlcjoxIn0=' 57 | ] 58 | assert await req.json() == [{}, [], {}] 59 | await handler.response(status=200) 60 | -------------------------------------------------------------------------------- /tests/swindon_lattice_inactivity.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from unittest import mock 4 | 5 | 6 | def assert_auth(req): 7 | assert req.path == '/swindon/authorize_connection' 8 | assert req.headers["Host"] == "swindon.internal" 9 | assert req.headers['Content-Type'] == 'application/json' 10 | assert re.match('^swindon/(\d+\.){2}\d+$', req.headers['User-Agent']) 11 | assert 'Authorization' not in req.headers 12 | 13 | 14 | def assert_headers(req): 15 | assert req.headers["Host"] == "swindon.internal" 16 | assert req.headers['Content-Type'] == 'application/json' 17 | assert re.match('^swindon/(\d+\.){2}\d+$', req.headers['User-Agent']) 18 | 19 | 20 | async def test_inactivity(proxy_server, swindon, loop): 21 | chat_url = swindon.url / 'swindon-lattice-w-timeouts' 22 | async with proxy_server() as proxy: 23 | handler = proxy.swindon_chat(chat_url, timeout=1) 24 | req = await handler.request() 25 | assert_auth(req) 26 | ws = await handler.json_response({ 27 | "user_id": 'user:1', "username": "Jim"}) 28 | 29 | hello = await ws.receive_json() 30 | assert hello == [ 31 | 'hello', {}, {'user_id': 'user:1', 'username': 'Jim'}] 32 | 33 | req = await handler.request(timeout=1.2) 34 | assert req.path == '/swindon/session_inactive' 35 | assert_headers(req) 36 | assert req.headers.getall('Authorization') == [ 37 | 'Tangle eyJ1c2VyX2lkIjoidXNlcjoxIn0=' 38 | ] 39 | assert await req.json() == [{}, [], {}] 40 | await handler.response(status=204) 41 | 42 | await ws.send_json([ 43 | 'whatever', {'request_id': '1', 'active': 2}, [], {}]) 44 | req = await handler.request(timeout=5) 45 | assert req.path == '/whatever' 46 | assert_headers(req) 47 | assert await req.json() == [ 48 | {'request_id': '1', 'active': 2, 'connection_id': mock.ANY}, 49 | [], {}] 50 | await handler.response(status=200) 51 | 52 | req = await handler.request(timeout=3.2) 53 | assert req.path == '/swindon/session_inactive' 54 | assert_headers(req) 55 | assert req.headers.getall('Authorization') == [ 56 | 'Tangle eyJ1c2VyX2lkIjoidXNlcjoxIn0=' 57 | ] 58 | assert await req.json() == [{}, [], {}] 59 | await handler.response(status=200) 60 | -------------------------------------------------------------------------------- /tests/websocket_echo_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | import aiohttp 4 | 5 | 6 | async def test_echo_chat(swindon, loop): 7 | url = swindon.url / 'websocket-echo' 8 | async with aiohttp.ClientSession(loop=loop) as s: 9 | async with s.ws_connect(url) as ws: 10 | await ws.send_str('Hello') 11 | assert await ws.receive_str() == 'Hello' 12 | 13 | await ws.send_bytes(b'How are you?') 14 | assert await ws.receive_bytes() == b'How are you?' 15 | 16 | await ws.send_json(["I'm", "fine", "thanks!"]) 17 | assert await ws.receive_json() == ["I'm", "fine", "thanks!"] 18 | 19 | with pytest.raises(asyncio.TimeoutError): 20 | assert await ws.receive_str(timeout=.1) is None 21 | 22 | await ws.ping() 23 | with pytest.raises(asyncio.TimeoutError): 24 | assert not await ws.receive(timeout=.1) 25 | --------------------------------------------------------------------------------