async def signature(request: requests.Request) -> None: # Only SHA1 is supported header_signature = request.headers.get("X-Hub-Signature") if header_signature is None: LOG.warning("Webhook without signature") raise fastapi.HTTPException(status_code=403) try: sha_name, signature = header_signature.split("=") except ValueError: sha_name = None if sha_name != "sha1": LOG.warning("Webhook signature malformed") raise fastapi.HTTPException(status_code=403) body = await request.body() current_hmac = utils.compute_hmac(body, config.WEBHOOK_SECRET) if hmac.compare_digest(current_hmac, str(signature)): return if config.WEBHOOK_SECRET_PRE_ROTATION is not None: future_hmac = utils.compute_hmac(body, config.WEBHOOK_SECRET_PRE_ROTATION) if hmac.compare_digest(future_hmac, str(signature)): return LOG.warning("Webhook signature invalid") raise fastapi.HTTPException(status_code=403)
def run(): parser = argparse.ArgumentParser() parser.add_argument("--clean", action="store_true") parser.add_argument("--dest", default="http://localhost:8802/event") args = parser.parse_args() logs.setup_logging() session = httpx.Client(trust_env=False) payload_data = os.urandom(250) payload_hmac = utils.compute_hmac(payload_data) if args.clean: # httpx doesn't allow data= here yet: https://github.com/encode/httpx/pull/900 r = session.request( "DELETE", "https://gh.mergify.io/events-testing", data=payload_data, headers={"X-Hub-Signature": "sha1=" + payload_hmac}, ) r.raise_for_status() while True: try: # httpx doesn't allow data= here yet: https://github.com/encode/httpx/pull/900 resp = session.request( "GET", "https://gh.mergify.io/events-testing", data=payload_data, headers={"X-Hub-Signature": "sha1=" + payload_hmac}, ) events = resp.json() for event in reversed(events): LOG.info("") LOG.info("==================================================") LOG.info( ">>> GOT EVENT: %s %s/%s", event["id"], event["type"], event["payload"].get("state", event["payload"].get("action")), ) data = json.dumps(event["payload"]) hmac = utils.compute_hmac(data.encode("utf8")) session.post( args.dest, headers={ "X-GitHub-Event": event["type"], "X-GitHub-Delivery": event["id"], "X-Hub-Signature": "sha1=%s" % hmac, "Content-type": "application/json", }, data=data, verify=False, ) except Exception: LOG.error("event handling failure", exc_info=True) time.sleep(1)
async def main(): parser = argparse.ArgumentParser() parser.add_argument("--clean", action="store_true") parser.add_argument("--dest", default="http://localhost:8802/event") args = parser.parse_args() logs.setup_logging() payload_data = os.urandom(250) payload_hmac = utils.compute_hmac(payload_data) async with http.AsyncClient( base_url="https://test-forwarder.mergify.io", headers={"X-Hub-Signature": "sha1=" + payload_hmac}, ) as session: if args.clean: r = await session.request("DELETE", "/events-testing", data=payload_data) r.raise_for_status() while True: try: resp = await session.request("GET", "/events-testing", data=payload_data) events = resp.json() for event in reversed(events): LOG.info("") LOG.info( "==================================================") LOG.info( ">>> GOT EVENT: %s %s/%s", event["id"], event["type"], event["payload"].get("state", event["payload"].get("action")), ) data = json.dumps(event["payload"]) hmac = utils.compute_hmac(data.encode("utf8")) await session.post( args.dest, headers={ "X-GitHub-Event": event["type"], "X-GitHub-Delivery": event["id"], "X-Hub-Signature": f"sha1={hmac}", "Content-type": "application/json", }, data=data, ) except Exception: LOG.error("event handling failure", exc_info=True) time.sleep(1)
def run(): parser = argparse.ArgumentParser() parser.add_argument('--clean', action='store_true') parser.add_argument('--dest', default="http://localhost:8802/event") args = parser.parse_args() utils.setup_logging() config.log() session = requests.Session() session.trust_env = False payload_data = os.urandom(250) payload_hmac = utils.compute_hmac(payload_data) if args.clean: r = session.delete("https://gh.mergify.io/events-testing", data=payload_data, headers={"X-Hub-Signature": "sha1=" + payload_hmac}) r.raise_for_status() while True: try: resp = session.get( "https://gh.mergify.io/events-testing", data=payload_data, headers={"X-Hub-Signature": "sha1=" + payload_hmac}, ) events = resp.json() for event in reversed(events): LOG.info("") LOG.info("==================================================") LOG.info( ">>> GOT EVENT: %s %s/%s", event['id'], event['type'], event['payload'].get("state", event['payload'].get("action"))) data = json.dumps(event['payload']) hmac = utils.compute_hmac(data.encode("utf8")) session.post(args.dest, headers={ "X-GitHub-Event": event['type'], "X-GitHub-Delivery": event['id'], "X-Hub-Signature": "sha1=%s" % hmac, "Content-type": "application/json", }, data=data, verify=False) except Exception: LOG.error("event handling failure", exc_info=True) time.sleep(1)
def test_market_event_forward(_, __, httpserver): with open( os.path.join(os.path.dirname(__file__), "events", "market_event.json")) as f: data = f.read() headers = { "X-GitHub-Delivery": str(uuid.uuid4()), "X-GitHub-Event": "purchased", "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data.encode()), "User-Agent": "GitHub-Hookshot/044aadd", "Content-Type": "application/json", } httpserver.expect_request("/", method="POST", data=data, headers=headers).respond_with_data("") with mock.patch( "mergify_engine.config.WEBHOOK_MARKETPLACE_FORWARD_URL", httpserver.url_for("/"), ): with testclient.TestClient(web.app) as client: client.post("/marketplace", data=data, headers=headers) httpserver.check_assertions()
def test_app_event_testing(): with open( os.path.join(os.path.dirname(__file__), "events", "push_event.json"), "rb" ) as f: data = f.read() headers = { "X-GitHub-Delivery": str(uuid.uuid4()), "X-GitHub-Event": "push", "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data), "User-Agent": "GitHub-Hookshot/044aadd", "Content-Type": "application/json", } with testclient.TestClient(web.app) as client: client.delete("/events-testing", data=data, headers=headers) client.post("/events-testing", data=data, headers=headers) client.post("/events-testing", data=data, headers=headers) client.post("/events-testing", data=data, headers=headers) client.post("/events-testing", data=data, headers=headers) client.post("/events-testing", data=data, headers=headers) events = client.get( "/events-testing?number=3", data=data, headers=headers ).json() assert 3 == len(events) events = client.get("/events-testing", data=data, headers=headers).json() assert 2 == len(events) events = client.get("/events-testing", data=data, headers=headers).json() assert 0 == len(events) client.post("/events-testing", data=data, headers=headers) client.delete("/events-testing", data=data, headers=headers) events = client.get("/events-testing", data=data, headers=headers).json() assert 0 == len(events)
def refresher() -> None: parser = argparse.ArgumentParser( description="Force refresh of mergify_engine") parser.add_argument("--action", default="user", choices=["user", "admin", "internal"]) parser.add_argument( "urls", nargs="*", help=("<owner>/<repo>, <owner>/<repo>/branch/<branch>, " "<owner>/<repo>/pull/<pull#> or " "https://github.com/<owner>/<repo>/pull/<pull#>"), ) args = parser.parse_args() if args.urls: for url in args.urls: url = url.replace("https://github.com/", "") data = secrets.token_hex(250) hmac = utils.compute_hmac(data.encode(), config.WEBHOOK_SECRET) asyncio.run( api_call( "POST", f"{config.BASE_URL}/refresh/{url}?action={args.action}", headers={"X-Hub-Signature": "sha1=" + hmac}, content=data, )) else: parser.print_help()
def api_call(url, method="post"): data = os.urandom(250) hmac = utils.compute_hmac(data) r = requests.request( method, url, headers={"X-Hub-Signature": "sha1=" + hmac}, data=data ) r.raise_for_status() print(r.text)
async def api_call(url, method="post"): data = os.urandom(250) hmac = utils.compute_hmac(data) async with http.AsyncClient() as client: r = await client.request( method, url, headers={"X-Hub-Signature": "sha1=" + hmac}, data=data ) r.raise_for_status() print(r.text)
def refresh(slug=None): data = os.urandom(250) hmac = utils.compute_hmac(data) url = config.BASE_URL + "/refresh" if slug: url = url + "/" + slug r = requests.post(url, headers={"X-Hub-Signature": "sha1=" + hmac}, data=data) r.raise_for_status() print(r.text)
def test_subscription_cache_delete(): installation_id = 123 data = None headers = { "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data), } with web.app.test_client() as client: reply = client.delete(f"/subscription-cache/{installation_id}", data=data, headers=headers) assert reply.status_code == 200 assert reply.data == b"Cache cleaned"
def test_subscription_cache_delete(): owner_id = 123 data = None headers = { "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data), } with testclient.TestClient(web.app) as client: reply = client.delete(f"/subscription-cache/{owner_id}", data=data, headers=headers) assert reply.status_code == 200 assert reply.content == b"Cache cleaned"
def test_subscription_cache_update(): installation_id = 123 charset = "utf-8" data = json.dumps({}).encode( charset) # for this test the content does not matter headers = { "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data), "Content-Type": f"application/json; charset={charset}", } with testclient.TestClient(web.app) as client: reply = client.put(f"/subscription-cache/{installation_id}", data=data, headers=headers) assert reply.status_code == 200 assert reply.content == b"Cache updated"
def test_push_event(event, event_type, status_code, reason): with testclient.TestClient(web.app) as client: charset = "utf-8" data = json.dumps(event).encode(charset) headers = { "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data), "X-GitHub-Event": event_type, "Content-Type": f"application/json; charset={charset}", } reply = client.post( "/event", data=data, headers=headers, ) assert reply.content == reason assert reply.status_code == status_code
def test_market_event_forward(mocked_http_client, _, __, ___): with open(os.path.dirname(__file__) + "/market_event.json", "rb") as f: data = f.read() headers = { "X-GitHub-Delivery": str(uuid.uuid4()), "X-GitHub-Event": "purchased", "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data), "User-Agent": "GitHub-Hookshot/044aadd", "Content-Type": "application/json", } with testclient.TestClient(web.app) as client: client.post("/marketplace", data=data, headers=headers) mocked_http_client.return_value.__enter__.return_value.post.assert_called_with( "https://foobar/engine/market", data=data, headers=headers)
def test_app_event_forward(mocked_requests_post, _, __, ___): with open(os.path.dirname(__file__) + "/push_event.json", "rb") as f: data = f.read() headers = { "X-GitHub-Delivery": str(uuid.uuid4()), "X-GitHub-Event": "push", "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data), "User-Agent": "GitHub-Hookshot/044aadd", "Content-Type": "application/json", } with web.app.test_client() as client: client.post("/event", data=data, headers=headers) mocked_requests_post.assert_called_with( "https://foobar/engine/app", data=data, headers=headers )
def authentification(): # pragma: no cover # Only SHA1 is supported header_signature = flask.request.headers.get('X-Hub-Signature') if header_signature is None: LOG.warning("Webhook without signature") flask.abort(403) try: sha_name, signature = header_signature.split('=') except ValueError: sha_name = None if sha_name != 'sha1': LOG.warning("Webhook signature malformed") flask.abort(403) mac = utils.compute_hmac(flask.request.data) if not hmac.compare_digest(mac, str(signature)): LOG.warning("Webhook signature invalid") flask.abort(403)
def test_subscription_cache_update(): owner_id = 123 charset = "utf-8" data = json.dumps({ "subscription_active": True, "subscription_reason": "Customer", "tokens": {}, "features": [], }).encode(charset) headers = { "X-Hub-Signature": "sha1=%s" % utils.compute_hmac(data), "Content-Type": f"application/json; charset={charset}", } with testclient.TestClient(web.app) as client: reply = client.put(f"/subscription-cache/{owner_id}", data=data, headers=headers) assert reply.status_code == 200 assert reply.content == b"Cache updated"
async def signature(request: requests.Request): # Only SHA1 is supported header_signature = request.headers.get("X-Hub-Signature") if header_signature is None: LOG.warning("Webhook without signature") raise fastapi.HTTPException(status_code=403) try: sha_name, signature = header_signature.split("=") except ValueError: sha_name = None if sha_name != "sha1": LOG.warning("Webhook signature malformed") raise fastapi.HTTPException(status_code=403) body = await request.body() mac = utils.compute_hmac(body) if not hmac.compare_digest(mac, str(signature)): LOG.warning("Webhook signature invalid") raise fastapi.HTTPException(status_code=403)
import vcr from mergify_engine import backports from mergify_engine import branch_updater from mergify_engine import check_api from mergify_engine import config from mergify_engine import utils from mergify_engine import web from mergify_engine import worker LOG = daiquiri.getLogger(__name__) RECORD = bool(os.getenv("MERGIFYENGINE_RECORD", False)) CASSETTE_LIBRARY_DIR_BASE = 'mergify_engine/tests/fixtures/cassettes' FAKE_DATA = "whatdataisthat" FAKE_HMAC = utils.compute_hmac(FAKE_DATA.encode("utf8")) class GitterRecorder(utils.Gitter): def __init__(self, cassette_library_dir, suffix): super(GitterRecorder, self).__init__() self.cassette_path = os.path.join(cassette_library_dir, "git-%s.json" % suffix) if RECORD: self.records = [] else: self.load_records() def load_records(self): if not os.path.exists(self.cassette_path): raise RuntimeError("Cassette %s not found" % self.cassette_path)
from mergify_engine import github_types from mergify_engine import gitter from mergify_engine import redis_utils from mergify_engine import utils from mergify_engine import worker from mergify_engine import worker_lua from mergify_engine.clients import github from mergify_engine.clients import http from mergify_engine.dashboard import subscription from mergify_engine.tests.functional import conftest as func_conftest from mergify_engine.web import root LOG = daiquiri.getLogger(__name__) RECORD = bool(os.getenv("MERGIFYENGINE_RECORD", False)) FAKE_DATA = "whatdataisthat" FAKE_HMAC = utils.compute_hmac(FAKE_DATA.encode("utf8"), config.WEBHOOK_SECRET) class ForwardedEvent(typing.TypedDict): payload: github_types.GitHubEvent type: github_types.GitHubEventType id: str class RecordException(typing.TypedDict): returncode: int output: str class Record(typing.TypedDict): args: typing.List[typing.Any]