From c1b90a2608be0642d48b32a72fc98de779bb1233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Abdelkader=20Mart=C3=ADnez=20P=C3=A9rez?= Date: Thu, 28 Mar 2019 07:40:21 +0100 Subject: [PATCH] Initial commit... about time. --- Pipfile | 18 ++++ Pipfile.lock | 300 +++++++++++++++++++++++++++++++++++++++++++++++++++ kapow.py | 246 ++++++++++++++++++++++++++++++++++++++++++ proposal.md | 130 ++++++++++++++++++++++ 4 files changed, 694 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 kapow.py create mode 100644 proposal.md diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..5c79506 --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +ipython = "*" +ipdb = "*" +pyparsing = "*" +jinja2 = "*" +aiohttp = "*" +click = "*" +aiofiles = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..1f530fa --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,300 @@ +{ + "_meta": { + "hash": { + "sha256": "5229faf1ac969cd54e8b83f073f080f632df013f9449ef21124bc67eeaa10f79" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiofiles": { + "hashes": [ + "sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee", + "sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d" + ], + "index": "pypi", + "version": "==0.4.0" + }, + "aiohttp": { + "hashes": [ + "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", + "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", + "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", + "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", + "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", + "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", + "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", + "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", + "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", + "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", + "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", + "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", + "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", + "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", + "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", + "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", + "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", + "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", + "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", + "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", + "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", + "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889" + ], + "index": "pypi", + "version": "==3.5.4" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "backcall": { + "hashes": [ + "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", + "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + ], + "version": "==0.1.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "index": "pypi", + "version": "==7.0" + }, + "decorator": { + "hashes": [ + "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", + "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" + ], + "version": "==4.4.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "ipdb": { + "hashes": [ + "sha256:dce2112557edfe759742ca2d0fee35c59c97b0cc7a05398b791079d78f1519ce" + ], + "index": "pypi", + "version": "==0.12" + }, + "ipython": { + "hashes": [ + "sha256:b038baa489c38f6d853a3cfc4c635b0cda66f2864d136fe8f40c1a6e334e2a6b", + "sha256:f5102c1cd67e399ec8ea66bcebe6e3968ea25a8977e53f012963e5affeb1fe38" + ], + "index": "pypi", + "version": "==7.4.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "jedi": { + "hashes": [ + "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b", + "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c" + ], + "version": "==0.13.3" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "index": "pypi", + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "multidict": { + "hashes": [ + "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", + "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3", + "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef", + "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b", + "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73", + "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc", + "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3", + "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd", + "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351", + "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941", + "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d", + "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1", + "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b", + "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a", + "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3", + "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7", + "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0", + "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0", + "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014", + "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5", + "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036", + "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d", + "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a", + "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce", + "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1", + "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a", + "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9", + "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7", + "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b" + ], + "version": "==4.5.2" + }, + "parso": { + "hashes": [ + "sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9", + "sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db" + ], + "version": "==0.3.4" + }, + "pexpect": { + "hashes": [ + "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", + "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.6.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", + "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", + "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" + ], + "version": "==2.0.9" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, + "pygments": { + "hashes": [ + "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", + "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + ], + "version": "==2.3.1" + }, + "pyparsing": { + "hashes": [ + "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a", + "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3" + ], + "index": "pypi", + "version": "==2.3.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "traitlets": { + "hashes": [ + "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", + "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" + ], + "version": "==4.3.2" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "yarl": { + "hashes": [ + "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9", + "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f", + "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb", + "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320", + "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842", + "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0", + "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829", + "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310", + "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4", + "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8", + "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1" + ], + "version": "==1.3.0" + } + }, + "develop": {} +} diff --git a/kapow.py b/kapow.py new file mode 100644 index 0000000..13d2bae --- /dev/null +++ b/kapow.py @@ -0,0 +1,246 @@ +from aiohttp import web +from dataclasses import dataclass +from shlex import quote as shell_quote +from string import Template +import asyncio +import contextlib +import io +import os +import re +import tempfile + +from pyparsing import * +import aiofiles +import click + +######################################################################## +# Parser # +######################################################################## + +# +# Endpoint Definition +# + +# Method +method = ( Literal('GET') + | Literal('POST') + | Literal('PUT') + | Literal('DELETE') + | Literal('PATCH') ) +multi_method = delimitedList(method, delim="|", combine=True) +method_spec = Combine(Literal('*') | multi_method) + +# Pattern +regex = Word(alphas + nums + '\\+*,[]-_')(name="regex") +symbol = Word(alphas)(name="symbol") +p_pattern = Combine('/{' + symbol + Optional(':' + regex) + '}') +p_path = Word('/', alphas + nums + '$-_.+!*\'(),') +urlpattern = Combine(OneOrMore(p_pattern | p_path))(name="urlpattern") + +# Body +body = (Suppress('{') + SkipTo(Combine(LineStart() + '}' + LineEnd()))(name="body")) + +# Endpoint +endpoint = (Optional(method_spec + Suppress(White()), + default='*')(name="method") + + urlpattern + + Suppress(White()) + + body)(name="endpoint") + +kapow_program = OneOrMore(endpoint) + + +@dataclass +class ResourceManager: + shell_repr: str + coro: object + + +async def get_value(context, path): + if path == 'request/method': + return context['request'].method.encode('utf-8') + elif path == 'request/path': + return context['request'].path.encode('utf-8') + elif path.startswith('request/match'): + key = path.split('/', 2)[-1] + return context['request'].match_info[key].encode('utf-8') + elif path.startswith('request/param'): + key = path.split('/', 2)[-1] + return context['request'].rel_url.query[key].encode('utf-8') + elif path.startswith('request/header'): + key = path.split('/', 2)[-1] + return context['request'].headers[key].encode('utf-8') + elif path.startswith('request/form'): + key = path.split('/', 2)[-1] + return (await context['request'].post())[key].encode('utf-8') + elif path == 'request/body': + return await context['request'].read() + else: + raise ValueError(f'Unknown path {path!r}') + +async def set_value(context, path, value): + if path == 'response/status': + context['response_status'] = int(value.decode('utf-8')) + elif path == 'response/body': + context['response_body'].write(value) + elif path.startswith('response/header/'): + key = path.split('/', 2)[-1] + context['response_headers'][key] = value.rstrip(b'\n').decode('utf-8') + else: + raise ValueError(f'Unknown path {path!r}') + +def is_readable(path): + return path.startswith('request/') + + +def is_writable(path): + return path.startswith('response/') + + +def get_manager(resource, context): + view, path = resource.split(':') + + @contextlib.asynccontextmanager + async def manager(): + if view == 'value': + if not is_readable(path): + raise ValueError(f'Non-readable path "{path}".') + else: + value = await get_value(context, path) + yield ResourceManager( + shell_repr=shell_quote(value.decode('utf-8')), + coro=asyncio.sleep(0)) + elif view == 'fifo': + # No race condition here? Shut your ass!! + # https://stackoverflow.com/a/1430566 + filename = tempfile.mktemp() + os.mkfifo(filename) + + async def manage_fifo(): + try: + if is_readable(path): + async with aiofiles.open(filename, 'wb') as fifo: + await fifo.write(await get_value(context, path)) + elif is_writable(path): + async with aiofiles.open(filename, 'rb') as fifo: + await set_value(context, path, await fifo.read()) + else: + raise RuntimeError('WTF!') + finally: + os.unlink(filename) + + yield ResourceManager( + shell_repr=shell_quote(filename), + coro=manage_fifo()) + + elif view == 'file': + with tempfile.NamedTemporaryFile(mode='w+b', buffering=0) as tmp: + if is_readable(path): + value = await get_value(context, path) + tmp.write(value) + tmp.flush() + + yield ResourceManager( + shell_repr=shell_quote(tmp.name), + coro=asyncio.sleep(0)) + + if is_writable(path): + tmp.seek(0) + await set_value(context, path, tmp.read()) + elif view == 'source': + raise NotImplementedError('source view not implemented') + elif view == 'sink': + raise NotImplementedError('sink view not implemented') + else: + raise ValueError(f'Unknown view type {view}') + + return manager + + +class KapowTemplate(Template): + delimiter = '@' + idpattern = r'(?a:[_a-z][_a-z0-9]*:[_a-z][-_a-z0-9/]*)' + + async def run(self, context): + async with contextlib.AsyncExitStack() as stack: + # Initialize all resources creating a mapping + resources = dict() # resource: (shell_repr, manager) + for match in self.pattern.findall(self.template): + _, resource, *_ = match + if resource not in resources: + manager = get_manager(resource, context) + resources[resource] = await stack.enter_async_context(manager()) + + code = self.substitute(**{k: v.shell_repr + for k, v in resources.items()}) + + if False: + print('-'*80) + print(code) + print('-'*80) + + shell_task = await asyncio.create_subprocess_shell(code) + manager_tasks = [asyncio.create_task(v.coro) + for v in resources.values()] + + await shell_task.wait() # Run the subshell process + done, pending = await asyncio.wait(manager_tasks, timeout=0) # Managers commit changes + if pending: + # print(f"Warning: Resources not consumed ({len(pending)})") + for task in pending: + task.cancel() + + +def create_runner(code): + return KapowTemplate(code).run + + +def create_context(request): + context = dict() + context["request"] = request + context["response_body"] = io.BytesIO() + context["response_status"] = 200 + context["response_headers"] = dict() + return context + + +def response_from_context(context): + return web.Response( + body=context["response_body"].getbuffer(), + status=context["response_status"], + headers=context["response_headers"]) + + +def generate_endpoint(code): + async def endpoint(request): + context = create_context(request) + runner = create_runner(code) + await runner(context) # Will change context + return response_from_context(context) + return endpoint + + +######################################################################## +# Webserver # +######################################################################## + +def register_endpoint(app, methods, pattern, code): + print(f"Registering methods={methods!r} pattern={pattern!r}") + endpoint = generate_endpoint(code) + for method in methods: # May be '*' + app.add_routes([web.route(method, pattern, endpoint)]) + + +@click.command() +@click.argument('program', type=click.File()) +def main(program): + app = web.Application() + for ep, _, _ in kapow_program.scanString(program.read()): + register_endpoint(app, + ep.method.asList()[0].split('|'), + ''.join(ep.urlpattern), + ep.body) + web.run_app(app) + +if __name__ == '__main__': + main() diff --git a/proposal.md b/proposal.md new file mode 100644 index 0000000..3ab2885 --- /dev/null +++ b/proposal.md @@ -0,0 +1,130 @@ +10-SECOND PROPOSAL +=================== + +*kapow* is a specialized language for marrying the **web** and the **shell**. + + +DESCRIPTION +=========== + +The **web** and the **shell** are two different beasts, both packed with history. + +There are some concepts in HTTP and the shell that **resemble each other**. + +``` + +------------------------+-------------------------+ + | HTTP | SHELL | + +--------------+------------------------+-------------------------+ + | Input | POST form-encoding | Command line parameters | + | Parameters | GET parameters | Environment variables | + | | Headers | | + | | Serialized body (JSON) | | + +--------------+------------------------+-------------------------+ + | Data Streams | Response/Request Body | Stdin/Stdout/Stderr | + | | Websocket | Input/Output files | + | | Uploaded files | | + +--------------+------------------------+-------------------------+ + | Control | Status codes | Signals | + | | HTTP Methods | | + +--------------+------------------------+-------------------------+ +``` + +Any tool designed to give an HTTP interface to an existing shell command **must map concepts of boths**. For example: + +- "GET parameters" to "Command line parameters" +- "Headers" to "Environment variables" +- "Stdout" to "Response body" + +*kapow* is not opinionated about the different ways you can map both worlds. Instead it provides a concise language used to express the mapping and a set of common defaults. + + +Why not tool...? +---------------- + +All the alternatives we found are **rigid** about how they match between HTTP and shell concepts. + +* [shell2http](https://github.com/msoap/shell2http): HTTP-server to execute shell commands. Designed for development, prototyping or remote control. Settings through two command line arguments, path and shell command. +* [websocketd](https://github.com/joewalnes/websocketd): Turn any program that uses STDIN/STDOUT into a WebSocket server. Like inetd, but for WebSockets. +* [webhook](https://github.com/adnanh/webhook): webhook is a lightweight incoming webhook server to run shell commands. +* [gotty](https://github.com/yudai/gotty): GoTTY is a simple command line tool that turns your CLI tools into web applications. (For interactive commands only) + +Tools with a rigid matching **can't evade** *[impedance mismatch](https://haacked.com/archive/2004/06/15/impedance-mismatch.aspx/)*. Resulting is an easy-to-use software, convenient in some scenarios but incapable in others. + + +Why not my good-old programming language...? +-------------------------------------------- + +* Boilerplate +* Custom code = More bugs +* Security issues (Command injection, etc) +* Dependency on developers +* "A programming language is low level when its programs require attention to the irrelevant" Alan Perlis + +*kapow* aims to be halfway from one of the mentioned tools and a general programming language. A limited scripting language. Think of *awk*, 20 years later, for HTTP. + + +Example +------- + +Imagine you want to improve your monitoring system to be able to check if a WiFi in a remote building is working. Suppose you already have a wireless capable linux server on that building. + +What about exposing a wifi scan as a service? + +``` +$ iwlist scan | grep 'ESSID:"BBVA"' +``` + +The above command will list the visible WiFI networks and filter for the one we are interested in (**BBVA**); exiting with code **0** when found and **1** otherwise. + +We want to signal the WiFI status with a meaningful HTTP status code: 200 for UP and 503 for DOWN. + +So the only thing our service has to do is translate from the **command's exit code** to a meaningful **HTTP status code**. + +Our *kapow* program (monitor.pow) should look **something like** this: + +``` +/monitor/wifi = + $(iwlist scan | grep 'ESSID:"BBVA') + | 0 = status 200 # OK + | 1 = status 503 # Service Unavailable +``` + +With this code, the user can run the service with: + +``` +$ kapow monitor.pow +``` +And then: + +1. *kapow* will open a port with an HTTP server serving the URI **/monitor/wifi**. +2. When requested `curl http:///monitor/wifi`. The command is executed resulting in a WiFI scan. +3. The command's **exit code** is translated to an HTTP **status code** and returned. + +USE CASES OR VALUE +================== + +* Reuse 40+ years of existing computer programs as microservices (nanoservices?). +* Expose command line only tools as services. Eliminating the need of interactive SSH sessions. +* Fine-grained access control to CLI based on options/parameters + + +REQUIRED SKILLS +=============== + +- Knowledge of HTTP and the command line. +- Programming. +- Low-level stuff... + + +ESTIMATION +========== + +- 2 weeks + + +OUTPUT +====== + +- A collection of **concrete** use cases, 5 to 10. +- A DSL design (over paper) to implement the use cases. +- A working **dirty** proof of concept.