diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..98dca648c2c4510337f3b6cf9f25e112f8a12530
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+clients.json
+examples/
+rmq.log
\ No newline at end of file
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000000000000000000000000000000000000..3956649ab33d5e6dd107b6583293810ddc7a2acd
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,20 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+pubsubpy = "*"
+pika = "*"
+pyrabbit = "*"
+flask = "*"
+flask-restful = "*"
+marshmallow = "*"
+bson = "*"
+flask-cors = "*"
+cryptography = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.8"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000000000000000000000000000000000000..5e5da41e3be47a76e766b2ab2bb56cf5c531482e
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,370 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "d216458acf3005c6f459eb1d74f7d5bbda1a7b5e1e042efe855739943c4674d4"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.8"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "amqp": {
+            "hashes": [
+                "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2",
+                "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==5.1.1"
+        },
+        "aniso8601": {
+            "hashes": [
+                "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f",
+                "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"
+            ],
+            "version": "==9.0.1"
+        },
+        "bson": {
+            "hashes": [
+                "sha256:d6511b2ab051139a9123c184de1a04227262173ad593429d21e443d6462d6590"
+            ],
+            "index": "pypi",
+            "version": "==0.5.10"
+        },
+        "cffi": {
+            "hashes": [
+                "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
+                "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
+                "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
+                "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
+                "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
+                "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
+                "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
+                "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
+                "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
+                "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
+                "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
+                "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
+                "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
+                "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
+                "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
+                "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
+                "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
+                "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
+                "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
+                "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
+                "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
+                "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
+                "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
+                "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
+                "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
+                "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
+                "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
+                "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
+                "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
+                "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
+                "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
+                "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
+                "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
+                "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
+                "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
+                "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
+                "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
+                "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
+                "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
+                "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
+                "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
+                "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
+                "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
+                "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
+                "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
+                "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
+                "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
+                "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
+                "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
+                "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
+                "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
+                "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
+                "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
+                "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
+                "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
+                "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
+                "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
+                "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
+                "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
+                "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
+                "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
+                "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
+                "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
+                "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
+            ],
+            "version": "==1.15.1"
+        },
+        "click": {
+            "hashes": [
+                "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
+                "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.1.3"
+        },
+        "cryptography": {
+            "hashes": [
+                "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d",
+                "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd",
+                "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146",
+                "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7",
+                "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436",
+                "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0",
+                "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828",
+                "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b",
+                "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55",
+                "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36",
+                "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50",
+                "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2",
+                "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a",
+                "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8",
+                "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0",
+                "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548",
+                "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320",
+                "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748",
+                "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249",
+                "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959",
+                "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f",
+                "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0",
+                "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd",
+                "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220",
+                "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c",
+                "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"
+            ],
+            "index": "pypi",
+            "version": "==38.0.3"
+        },
+        "flask": {
+            "hashes": [
+                "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b",
+                "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"
+            ],
+            "index": "pypi",
+            "version": "==2.2.2"
+        },
+        "flask-cors": {
+            "hashes": [
+                "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438",
+                "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"
+            ],
+            "index": "pypi",
+            "version": "==3.0.10"
+        },
+        "flask-restful": {
+            "hashes": [
+                "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2",
+                "sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e"
+            ],
+            "index": "pypi",
+            "version": "==0.3.9"
+        },
+        "future": {
+            "hashes": [
+                "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
+            ],
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==0.18.2"
+        },
+        "httplib2": {
+            "hashes": [
+                "sha256:987c8bb3eb82d3fa60c68699510a692aa2ad9c4bd4f123e51dfb1488c14cdd01",
+                "sha256:fc144f091c7286b82bec71bdbd9b27323ba709cc612568d3000893bfd9cb4b34"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==0.21.0"
+        },
+        "importlib-metadata": {
+            "hashes": [
+                "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b",
+                "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"
+            ],
+            "markers": "python_version < '3.10'",
+            "version": "==5.1.0"
+        },
+        "itsdangerous": {
+            "hashes": [
+                "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
+                "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.2"
+        },
+        "jinja2": {
+            "hashes": [
+                "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
+                "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.1.2"
+        },
+        "kombu": {
+            "hashes": [
+                "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610",
+                "sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==5.2.4"
+        },
+        "markupsafe": {
+            "hashes": [
+                "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
+                "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
+                "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
+                "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
+                "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
+                "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
+                "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
+                "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
+                "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
+                "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
+                "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
+                "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
+                "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
+                "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
+                "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
+                "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
+                "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
+                "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
+                "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
+                "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
+                "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
+                "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
+                "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
+                "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
+                "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
+                "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
+                "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
+                "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
+                "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
+                "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
+                "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
+                "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
+                "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
+                "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
+                "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
+                "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
+                "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
+                "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
+                "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
+                "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.1"
+        },
+        "marshmallow": {
+            "hashes": [
+                "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78",
+                "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"
+            ],
+            "index": "pypi",
+            "version": "==3.19.0"
+        },
+        "packaging": {
+            "hashes": [
+                "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
+                "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==21.3"
+        },
+        "pika": {
+            "hashes": [
+                "sha256:89f5e606646caebe3c00cbdbc4c2c609834adde45d7507311807b5775edac8e0",
+                "sha256:beb19ff6dd1547f99a29acc2c6987ebb2ba7c44bf44a3f8e305877c5ef7d2fdc"
+            ],
+            "index": "pypi",
+            "version": "==1.3.1"
+        },
+        "pubsubpy": {
+            "hashes": [
+                "sha256:58e394d14dd172fc03caff172adf3817d980bb6b8cb46cd18a362f8aa6e530c6",
+                "sha256:b29fa140615935ac03801ccd1de137ce4d33b741465b9002f290538ce966f2e9"
+            ],
+            "index": "pypi",
+            "version": "==2.3.0"
+        },
+        "pycparser": {
+            "hashes": [
+                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
+            ],
+            "version": "==2.21"
+        },
+        "pyparsing": {
+            "hashes": [
+                "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
+                "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
+            ],
+            "markers": "python_full_version >= '3.6.8'",
+            "version": "==3.0.9"
+        },
+        "pyrabbit": {
+            "hashes": [
+                "sha256:50b8995fbfde14820ddc97292312c8f0c77054748c2b018138d03d94e400c39c"
+            ],
+            "index": "pypi",
+            "version": "==1.1.0"
+        },
+        "python-dateutil": {
+            "hashes": [
+                "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+                "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.8.2"
+        },
+        "pytz": {
+            "hashes": [
+                "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427",
+                "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"
+            ],
+            "version": "==2022.6"
+        },
+        "six": {
+            "hashes": [
+                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.16.0"
+        },
+        "vine": {
+            "hashes": [
+                "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
+                "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==5.0.0"
+        },
+        "werkzeug": {
+            "hashes": [
+                "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f",
+                "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.2.2"
+        },
+        "zipp": {
+            "hashes": [
+                "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1",
+                "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.10.0"
+        }
+    },
+    "develop": {}
+}
diff --git a/README.md b/README.md
index f4374047eb3d69ef26107ec97983ba0790f1bf06..fdd1d8c6eabedc62f68e586631a22caf790b6824 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,93 @@
 # Communications Backbone
 
-Communications Backbone by C2 Team (NOC)
\ No newline at end of file
+Communications Backbone by C2 Team (NOC)
+
+## DJ prototype 
+
+I did this in freedom-fire-day so it doesn't currently follow the flask template. 
+
+It's also a bit weird because it writes to a local ignored `clients.json` file 
+instead of using a database. I did this as a placeholder because we've not yet 
+decided what the infrastructure looks like.
+
+### Data flow 
+
+- Client A sends to `client-a-outbox` (or POSTs to API /send - not yet implemented)
+- Messages are forwarded from `client-a-outbox` to `soar-publish` 
+- Messages are published from `soar-publish` with the given topic read from the message 
+- Subscribers listen to for messages 
+- Subscription is delivered to `client-b-inbox` 
+- Client B reads from `client-b-inbox (or GETs from API /receive)
+
+There is a parallel flow when a client sends to `client-a-notify` in which case the 
+messages are delivered through the broadcast exchange to all clients `client-x-inbox`.
+
+### Auth placeholder 
+
+As a proxy for proper authentication, when you post a client a random secret is 
+returned in the response. To send to / receive from the bus you then call the API
+with the client_id and secret and it checks they match. The client_id determines 
+which queues it reads from. 
+
+Subsequent requests to the client endpoint return the client_id but not the secret.
+
+### Setup 
+
+```
+pipenv install  
+```
+
+### Running 
+
+#### RabbitMQ 
+
+`docker run --rm -p 5672:5672 -d --hostname rmq --name rmq rabbitmq:management`
+
+#### API 
+
+```
+pipenv run python api.py 
+```
+
+#### Create some clients 
+
+`POST` to `http://localhost:3000/clients` 
+
+#### Event bus 
+
+``` 
+pipenv run python soar_bus.py
+```
+
+#### Send / Receive directly 
+
+```
+# Send a message 
+pipenv run python client_send.py noc-c2-outbox 'soar.noc.slocum.something' from noc-c2
+```
+
+```
+# Receive messages  
+pipenv run python client_read.py noc-sfmc-inbox
+```
+
+#### Receive via API
+
+As a placeholder for proper authentication you post the `client_id` and 
+`secret` and it checks that a client with that id exists and that the 
+secret matches before allowing the request. 
+
+This should be replaced with a proper auth layer. 
+
+`GET http://localhost:5000/receive?client_id=[client_id]&secret=[secret]`
+
+### Components 
+
+- `soar_bus.py` - Run all the components threaded based on existing clients  
+- `soar_forward.py` - Listen for messages on queue A and forward messages to queue B 
+- `soar_publish.py` - Listen for messages on queue A and publish on exchange B 
+- `soar_broadcast.py` - Listen for messages on queue A and broadcast on exchange B 
+- `soar_subscribe.py` - Create subscriptions to both the publish and broadcast exchange - deliver to queue A
+  (I think this should probably be 2 separate functions to keep things nice and simple)
+- `soar_push.py` - Not yet implemented - Listen for messages on queue A and POST to the client's webhook URL 
+
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api.py b/api.py
new file mode 100644
index 0000000000000000000000000000000000000000..064555687ee72e96b3156f2d81f98cba705f225e
--- /dev/null
+++ b/api.py
@@ -0,0 +1,26 @@
+from flask import Flask
+from flask_restful import Api
+from endpoints.clients import Client, ClientList
+from endpoints.receive import Receive
+from endpoints.send import Send
+from endpoints.notify import Notify
+from endpoints.token import Token
+from flask_cors import CORS
+from models.token import TokenModel
+
+token = TokenModel()
+token.setSecret()
+
+app = Flask(__name__)
+api = Api(app)
+CORS(app, resources={r"*": {"origins": "http://localhost:8086"}})
+
+api.add_resource(ClientList, "/client")
+api.add_resource(Client, "/client/<client_id>")
+api.add_resource(Receive, "/receive")
+api.add_resource(Send, "/send")
+api.add_resource(Notify, "/notify")
+api.add_resource(Token, "/token")
+
+if __name__ == "__main__":
+    app.run(debug=True, port=8087)
diff --git a/client_read.py b/client_read.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe860600bd0efc63cbf8eba2b87547e1333784a6
--- /dev/null
+++ b/client_read.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+import pika, sys, os, json
+
+
+def main():
+    connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
+    channel = connection.channel()
+
+    queue_name = sys.argv[1]
+
+    channel.queue_declare(queue=queue_name, durable=True)
+
+    def callback(ch, method, properties, body):
+        message = json.loads(body.decode())
+        print(" [x] Received %r" % message)
+
+    channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
+
+    print(" [*] Waiting for messages. To exit press CTRL+C")
+    channel.start_consuming()
+
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print("Interrupted")
+        try:
+            sys.exit(0)
+        except SystemExit:
+            os._exit(0)
diff --git a/client_send.py b/client_send.py
new file mode 100644
index 0000000000000000000000000000000000000000..42197be815668bbf2a21fe3d679e6accf8bfb521
--- /dev/null
+++ b/client_send.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+import pika
+import sys
+import json
+
+
+connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
+channel = connection.channel()
+
+queue_name = sys.argv[1]
+topic = sys.argv[2]
+message = " ".join(sys.argv[3:]) or "Hello World!"
+body = json.dumps({"topic": topic, "message": message})
+channel.queue_declare(queue=queue_name, durable=True)
+channel.basic_publish(exchange="", routing_key=queue_name, body=body)
+connection.close()
diff --git a/endpoints/__init__.py b/endpoints/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/endpoints/auth_resource.py b/endpoints/auth_resource.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ebd2019ecabb8d29407de80b42a7f8a35c59176
--- /dev/null
+++ b/endpoints/auth_resource.py
@@ -0,0 +1,25 @@
+import json
+from flask_restful import Resource, abort
+from models.token import TokenModel
+
+class AuthResource(Resource):
+
+    def __init__(self): 
+        self.token = TokenModel()
+        with open("clients.json", "r") as clients_file:
+            self.clients = json.load(clients_file)
+
+    def auth(self, request): 
+        allow = False
+        auth = request.headers.get('Authorization', False)
+        if auth:
+            token = auth.split(' ').pop()
+            parsed = self.token.validate(token)
+            if parsed['valid']:
+                client = self.clients.get(parsed['client_id'])
+                if client: 
+                    self.client = client
+                    allow = True
+        if not allow:
+            abort(403, message="Invalid token")
+        return allow
\ No newline at end of file
diff --git a/endpoints/clients.py b/endpoints/clients.py
new file mode 100644
index 0000000000000000000000000000000000000000..a079e934d451dd14a85251c4f94d6d9faa669058
--- /dev/null
+++ b/endpoints/clients.py
@@ -0,0 +1,140 @@
+from flask_restful import Resource, request, abort
+from marshmallow import Schema, fields
+import json
+import os
+import random
+import string
+
+
+class ClientSchema(Schema):
+    client_id = fields.Str(required=True)
+    client_name =  fields.Str(required=True)
+    subscription = fields.Str(required=True)
+
+
+class ClientsFile:
+    file = "clients.json"
+    mtime = 0
+    clients = {}
+    parser = None
+
+    def __init__(self):
+        self.get()
+
+    def get(self):
+        try:
+            mtime = os.path.getmtime(self.file)
+            if mtime > self.mtime:
+                with open(self.file, "r") as client_file:
+                    self.clients = json.load(client_file)
+        except FileNotFoundError as error:
+            self.clients = {}
+            self.save()
+
+        return self.clients
+
+    def find(self, client_id):
+        self.get()
+        if client_id in self.clients:
+            client = self.clients[client_id]
+        else:
+            client = None
+        return client
+
+    def add(self, client):
+        client['secret'] = self.secret()
+        self.clients[client["client_id"]] = client
+        self.save()
+        return client
+
+    def remove(self, client):
+        del self.clients[client["client_id"]]
+        self.save()
+
+    def update(self, client_updates):
+        client = self.find(client_updates["client_id"])
+        client.update(client_updates)
+        self.clients[client["client_id"]] = client
+        self.save()
+        return client
+
+    def save(self):
+        try:
+            with open(self.file, "w") as client_file:
+                client_file.write(json.dumps(self.clients, indent=2))
+                return True
+        except OSError as error:
+            print(str(error))
+            return False
+
+    def secret(self, chars=36):
+        res = "".join(
+            random.choices(
+                string.ascii_lowercase + string.ascii_uppercase + string.digits, k=chars
+            )
+        )
+        return str(res)
+
+clients_file = ClientsFile()
+
+# Client
+class Client(Resource):
+    clients_file = None
+    def __init__(self): 
+        self.schema = ClientSchema()
+        self.clients_file = ClientsFile()
+
+    def get(self, client_id):
+        client = self.clients_file.find(client_id)
+        del client['secret']
+        if not client:
+            abort(404, message="No client with id: {}".format(client_id))
+        return client
+
+    def delete(self, todo_id):
+        client = self.clients_file.find(client_id)
+        if not client:
+            abort(404, message="No client with id: {}".format(client_id))
+        else:
+            self.clients_file.remove(client)
+        return client, 204
+
+    def put(self, client_id):
+        args = request.get_json()
+        errors = self.schema.validate(args)
+        if errors: 
+            abort(400, message=str(errors))
+        
+        client = self.clients_file.find(client_id)
+        if not client:
+            abort(404, message="No client with id: {}".format(client_id))
+        else:
+            client = self.clients_file.update(args)
+        return client, 201
+
+
+# ClientList
+class ClientList(Resource):
+    def __init__(self): 
+        self.schema = ClientSchema()
+        self.clients_file = ClientsFile()
+
+    def get(self):
+        return {
+            client_id: (client, client.pop("secret", None))[0]
+            for client_id, client in self.clients_file.get().items()
+        }
+
+    def post(self):
+        args = request.get_json()
+
+        errors = self.schema.validate(args)
+        if errors: 
+            abort(400, message=str(errors))
+        
+        client = clients_file.find(args["client_id"])
+        if client:
+            abort(403, message="Duplicate client id: {}".format(client_id))
+        else:
+            client = clients_file.add(args)
+        return client, 201
diff --git a/endpoints/hello.py b/endpoints/hello.py
new file mode 100644
index 0000000000000000000000000000000000000000..7bb0bab0de35c7cebf2d22fac1f1213b16ef90df
--- /dev/null
+++ b/endpoints/hello.py
@@ -0,0 +1,6 @@
+from flask_restful import Resource
+
+
+class HelloWorld(Resource):
+    def get(self):
+        return {"hello": "world"}
diff --git a/endpoints/notify.py b/endpoints/notify.py
new file mode 100644
index 0000000000000000000000000000000000000000..a953db879eee9b7b7b22ce34de44e57f93d9e772
--- /dev/null
+++ b/endpoints/notify.py
@@ -0,0 +1,40 @@
+import json
+from flask_restful import request, abort
+from marshmallow import Schema, fields
+import pika
+from endpoints.auth_resource import AuthResource
+
+
+class NotifySchema(Schema):
+    body = fields.Str(required=True)
+
+class Notify(AuthResource):
+    clients = None
+    schema = None
+
+    def __init__(self):
+        super().__init__()
+        self.schema = NotifySchema()
+   
+    def post(self):
+        args = request.get_json()
+        errors = self.schema.validate(args)
+        if errors:
+            abort(400, message=str(errors))
+
+        allow = False
+        body = args.get("body")
+        message = {
+            'topic': 'broadcast',
+            'message': body,
+        }
+
+        allow = self.auth(request)
+        
+        if allow:
+            notify_queue = self.client['client_id'] + "-broadcast"
+            connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
+            channel = connection.channel()
+            channel.queue_declare(queue=notify_queue, durable=True)
+            channel.basic_publish(exchange="", routing_key=notify_queue, body=json.dumps(message))
+            connection.close()
diff --git a/endpoints/receive.py b/endpoints/receive.py
new file mode 100644
index 0000000000000000000000000000000000000000..bc1febaa41d5fbd2e724c2bf77521842952c6c88
--- /dev/null
+++ b/endpoints/receive.py
@@ -0,0 +1,50 @@
+from flask_restful import request, abort
+from marshmallow import Schema, fields
+import pika
+import json
+from models.token import TokenModel
+from endpoints.auth_resource import AuthResource
+
+
+class ReceiveQuerySchema(Schema):
+    max_messages = fields.Int(required=False)
+
+
+class Receive(AuthResource):
+    clients = None
+    schema = None
+
+    def __init__(self):
+        super().__init__()
+        self.schema = ReceiveQuerySchema()
+
+    def get(self):
+        errors = self.schema.validate(request.args)
+        if errors:
+            abort(400, message=str(errors))
+
+        messages = []
+        max_messages = request.args.get("max_messages", 10)
+        
+        allow = self.auth(request)
+        if allow:
+            inbox_queue = self.client['client_id'] + "-inbox"
+    
+        if allow:
+            connection = pika.BlockingConnection(
+                pika.ConnectionParameters(host="localhost")
+            )
+            channel = connection.channel()
+            channel.queue_declare(queue=inbox_queue, durable=True)
+            while len(messages) < max_messages:
+                method_frame, header_frame, body = channel.basic_get(inbox_queue)
+                if method_frame:
+                    print(method_frame, header_frame, body)
+                    channel.basic_ack(method_frame.delivery_tag)
+                    messages.append(json.loads(body.decode()))
+                else:
+                    print("No message returned")
+                    break
+            channel.close()
+            connection.close()
+        return messages
diff --git a/endpoints/send.py b/endpoints/send.py
new file mode 100644
index 0000000000000000000000000000000000000000..900e8a5c63d066921315e9e93e22fb8d4eb59d9a
--- /dev/null
+++ b/endpoints/send.py
@@ -0,0 +1,40 @@
+import json
+from flask_restful import request, abort
+from marshmallow import Schema, fields
+import pika
+from endpoints.auth_resource import AuthResource
+
+class SendSchema(Schema):
+    body = fields.Str(required=True)
+    topic = fields.Str(required=True)
+
+class Send(AuthResource):
+    clients = None
+    schema = None
+
+    def __init__(self):
+        super().__init__()
+        self.schema = SendSchema()
+    
+    def post(self):
+        args = request.get_json()
+        errors = self.schema.validate(args)
+        if errors:
+            abort(400, message=str(errors))
+
+        allow = self.auth(request)
+        
+        if allow:
+            body = args.get("body")
+            topic = args.get("topic")
+            outbox_queue = self.client['client_id'] + "-outbox"
+        
+            connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
+            channel = connection.channel()
+            channel.queue_declare(queue=outbox_queue, durable=True)
+            message = {
+                'topic': topic,
+                'message': body,
+            }
+            channel.basic_publish(exchange="", routing_key=outbox_queue, body=json.dumps(message))
+            connection.close()
diff --git a/endpoints/token.py b/endpoints/token.py
new file mode 100644
index 0000000000000000000000000000000000000000..01dbedf3f94c83c5663f9fb25c7fc7d9e6f7c797
--- /dev/null
+++ b/endpoints/token.py
@@ -0,0 +1,43 @@
+import json 
+from flask_restful import Resource, request, abort
+from marshmallow import Schema, fields
+import pika
+from models.token import TokenModel
+
+
+class TokenQuerySchema(Schema):
+    client_id = fields.Str(required=True)
+    secret = fields.Str(required=True)
+
+
+class Token(Resource):
+    clients = None
+    schema = None
+    model = None
+
+    def __init__(self):
+        self.schema = TokenQuerySchema()
+        self.model = TokenModel()
+        with open("clients.json", "r") as clients_file:
+            self.clients = json.load(clients_file)
+    
+    def get(self):
+        errors = self.schema.validate(request.args)
+        if errors:
+            abort(400, message=str(errors))
+
+        token = None
+        allow = False
+        max_messages = request.args.get("max_messages", 10)
+        client_id = request.args.get("client_id")
+        if client_id in self.clients:
+            client = self.clients.get(client_id)
+            if request.args.get("secret") == client.get("secret"):
+                allow = True
+
+        if allow:
+            token = self.model.get(client_id)
+
+        else:
+            abort(403, message="Invalid client credentials")
+        return token
\ No newline at end of file
diff --git a/models/token.py b/models/token.py
new file mode 100644
index 0000000000000000000000000000000000000000..6721a9971abe59a20d249f52e5bddbe8d4974acc
--- /dev/null
+++ b/models/token.py
@@ -0,0 +1,90 @@
+from cryptography.fernet import Fernet,InvalidToken
+import datetime
+import os
+import json
+
+
+TOKENS = {}
+
+
+class TokenModel():
+    clients = None
+    schema = None
+    key = None
+    fernet = None
+    token_lifetime_hours = None
+    env_lifetime = 'SOAR_TOKEN_LIFETIME'
+    env_secret = 'SOAR_TOKEN_SECRET'
+
+    def __init__(self):
+        self.getFernet()
+        self.token_lifetime_hours = os.getenv(self.env_lifetime, 24)
+    
+    def getFernet(self): 
+        self.fernet = Fernet(self.getKey().encode()) 
+
+    def getKey(self): 
+        key = os.getenv(self.env_secret)
+        print(key)
+        if not key: 
+            key = Fernet.generate_key().decode()
+            os.environ[self.env_secret] = key
+        self.key = key    
+        return self.key
+
+    def setSecret(self): 
+        if not os.getenv(self.env_secret):
+            os.environ[self.env_secret] = self.getKey() 
+
+    def getExpiry(self): 
+        now = datetime.datetime.utcnow()
+        expires = now + datetime.timedelta(hours=self.token_lifetime_hours)
+        return expires.isoformat()
+    
+    def encrypt(self, client_id):
+        try:
+            expiry = self.getExpiry()
+            token_content = {
+                'client_id': client_id,
+                'expiry': expiry 
+            }
+            token = self.fernet.encrypt(json.dumps(token_content).encode()).decode()
+            return {
+                'token': token,
+                'expiry': expiry
+            }
+        except KeyError as e: 
+            return None
+    
+    def decrypt(self, token):
+        try:  
+            content = json.loads(self.fernet.decrypt(token.encode()).decode())
+            return content 
+        except (InvalidToken,KeyError) as e:
+            return None
+
+    def get(self, client_id):
+        response = self.encrypt(client_id)
+        TOKENS[response['token']] = client_id
+        return response 
+
+    def validate(self, token):
+        response = {
+            'valid': False
+        }
+        if token in TOKENS:
+            content = self.decrypt(token)
+            if content:
+                now = datetime.datetime.utcnow()
+                expires = datetime.datetime.fromisoformat(content['expiry'])
+                response['valid'] = expires > now
+                if response['valid']:
+                    response.update(content)
+                else: 
+                    del TOKENS[token]
+            else:
+                del TOKENS[token]
+        return response 
+
+
+
diff --git a/soar_broadcast.py b/soar_broadcast.py
new file mode 100644
index 0000000000000000000000000000000000000000..de5062db99981096a513d372b2acd6dcd0fa28d4
--- /dev/null
+++ b/soar_broadcast.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+import pika
+
+
+def get_connection():
+    return pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
+
+
+def deliver(body, broadcast_exchange):
+    print("broadcast")
+    deliver_connection = get_connection()
+    deliver_channel = deliver_connection.channel()
+    deliver_channel.exchange_declare(
+        exchange=broadcast_exchange, exchange_type="fanout"
+    )
+    deliver_channel.basic_publish(
+        exchange=broadcast_exchange, routing_key="", body=body
+    )
+    deliver_connection.close()
+
+
+def listen(queue_name, broadcast_exchange):
+    def bcast_callback(ch, method, properties, body):
+        delivered = deliver(body, broadcast_exchange)
+        ch.basic_ack(delivery_tag=method.delivery_tag)
+
+    listen_connection = get_connection()
+    listen_channel = listen_connection.channel()
+    listen_channel.queue_declare(queue=queue_name, durable=True)
+    listen_channel.basic_consume(queue=queue_name, on_message_callback=bcast_callback)
+    listen_channel.start_consuming()
+
+
+def broadcast(queue_name, broadcast_exchange="soar_broadcast"):
+    listen(queue_name, broadcast_exchange)
diff --git a/soar_bus.py b/soar_bus.py
new file mode 100644
index 0000000000000000000000000000000000000000..3576ea3a75a390ec4cd2de7ce111d7435d5eabf4
--- /dev/null
+++ b/soar_bus.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+
+# Queues
+# (per client)
+# [client_id]-inbox - receive subscriptions
+# [client_id]-outbox - send messages to the bus
+# [client-id]-broadcast - send message to all subscribers? - eg notify of downtime
+# soar-publisher - fan-in from client-outboxes
+# soar-dlq - undeliverables
+# soar-broadcast - admin messages forwarded to all client-inboxes regardless of subscriptions
+
+import concurrent.futures
+from endpoints.clients import ClientsFile
+from soar_broadcast import broadcast
+from soar_forward import forward
+from soar_publish import publish
+from soar_subscribe import subscribe
+
+THREADS = []
+EXCHANGES = {
+    "publish": "soar_publish",
+    "broadcast": "soar_broadcast",
+}
+
+
+def main():
+    clients_file = ClientsFile()
+    clients = clients_file.get()
+
+    with concurrent.futures.ProcessPoolExecutor() as executor:
+        # publish
+        thread = executor.submit(publish, "soar-publish", EXCHANGES.get("publish"))
+        THREADS.append(thread)
+
+        for (id, client) in clients.items():
+            # forward
+            thread = executor.submit(forward, f"{id}-outbox", "soar-publish")
+            THREADS.append(thread)
+            # broadcast
+            thread = executor.submit(
+                broadcast, f"{id}-broadcast", EXCHANGES.get("broadcast")
+            )
+            THREADS.append(thread)
+            # subscribe
+            thread = executor.submit(
+                subscribe,
+                f"{id}-inbox",
+                client["subscription"],
+                EXCHANGES.get("publish"),
+                EXCHANGES.get("broadcast"),
+            )
+            THREADS.append(thread)
+            # push
+            # TODO - add optional webhook target to client and post to webhook target
+            # if present
+
+
+if __name__ == "__main__":
+    main()
diff --git a/soar_forward.py b/soar_forward.py
new file mode 100644
index 0000000000000000000000000000000000000000..855d02f30ff49adeeaaa7e031cfb7aeca7812af4
--- /dev/null
+++ b/soar_forward.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+import pika
+
+
+def get_connection():
+    return pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
+
+
+def deliver(body, queue_name):
+    print("forward to: %s" % queue_name)
+    deliver_connection = get_connection()
+    deliver_channel = deliver_connection.channel()
+    deliver_channel.queue_declare(queue=queue_name, durable=True)
+    deliver_channel.basic_publish(exchange="", routing_key=queue_name, body=body)
+    deliver_connection.close()
+
+
+def listen(from_queue_name, to_queue_name):
+    def fwd_callback(ch, method, properties, body):
+        delivered = deliver(body, to_queue_name)
+        ch.basic_ack(delivery_tag=method.delivery_tag)
+
+    listen_connection = get_connection()
+    listen_channel = listen_connection.channel()
+    listen_channel.queue_declare(queue=from_queue_name, durable=True)
+    listen_channel.basic_consume(
+        queue=from_queue_name, on_message_callback=fwd_callback
+    )
+    listen_channel.start_consuming()
+
+
+def forward(from_queue_name, to_queue_name):
+    listen(from_queue_name, to_queue_name)
diff --git a/soar_publish.py b/soar_publish.py
new file mode 100644
index 0000000000000000000000000000000000000000..52121faae63eead99d410d4534e3888f11842eb0
--- /dev/null
+++ b/soar_publish.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+import pika
+import json
+import sys
+
+
+def get_connection():
+    return pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
+
+
+def deliver(body, topic, publish_exchange):
+    print("publish on topic: %s" % topic)
+    deliver_connection = get_connection()
+    deliver_channel = deliver_connection.channel()
+    deliver_channel.exchange_declare(exchange=publish_exchange, exchange_type="topic")
+    deliver_channel.basic_publish(
+        exchange=publish_exchange, routing_key=topic, body=body
+    )
+    deliver_connection.close()
+
+
+def listen(queue_name, publish_exchange):
+    def pub_callback(ch, method, properties, body):
+        message = json.loads(body.decode())
+        topic = message["topic"]
+        deliver(body, topic, publish_exchange)
+        ch.basic_ack(delivery_tag=method.delivery_tag)
+
+    listen_connection = get_connection()
+    listen_channel = listen_connection.channel()
+    listen_channel.queue_declare(queue=queue_name, durable=True)
+    listen_channel.basic_consume(queue=queue_name, on_message_callback=pub_callback)
+    listen_channel.start_consuming()
+
+
+def publish(queue_name, publish_exchange="soar_publish"):
+    listen(queue_name, publish_exchange)
+
+
+if __name__ == "__main__":
+    queue_name = sys.argv[1]
+    publish(queue_name)
diff --git a/soar_push.py b/soar_push.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/soar_subscribe.py b/soar_subscribe.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ce847738e26c9ccc4530c5b0476b98d0f0b9cff
--- /dev/null
+++ b/soar_subscribe.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+import pika
+
+
+def get_connection():
+    return pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
+
+
+def subscribe(
+    queue_name,
+    topic,
+    publish_exchange="soar_publish",
+    broadcast_exchange="soar_broadcast",
+):
+    adm_connection = get_connection()
+    admin_channel = adm_connection.channel()
+    admin_channel.exchange_declare(exchange=broadcast_exchange, exchange_type="fanout")
+    admin_channel.queue_bind(exchange=broadcast_exchange, queue=queue_name)
+    sub_connection = get_connection()
+    subscriber_channel = sub_connection.channel()
+    subscriber_channel.exchange_declare(
+        exchange=publish_exchange, exchange_type="topic"
+    )
+    subscriber_channel.queue_bind(
+        exchange=publish_exchange, queue=queue_name, routing_key=topic
+    )