From 6caaafe1ea565dd871aeff0f82c4279fdfd5042c Mon Sep 17 00:00:00 2001 From: Lucas Noki Date: Tue, 14 Jun 2022 20:59:16 +0200 Subject: [PATCH 01/10] Change version from python:3.7-slim to 3.9-buster to fix dependency errors on docker build, Change pip to pip3 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9a4fb7b..f8d032d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM python:3.7-slim +FROM python:3.9-buster WORKDIR /root/fireprox COPY . . -RUN pip install -r requirements.txt +RUN pip3 install -r requirements.txt ENTRYPOINT ["python", "/root/fireprox/fire.py"] From 6d5171e45b47f4fd08a118ee10806c5070b221ce Mon Sep 17 00:00:00 2001 From: ustayready Date: Tue, 6 Sep 2022 09:42:42 -0400 Subject: [PATCH 02/10] Updated usage --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 8054532..8958764 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,20 @@ usage: **fire.py** [-h] [--access_key ACCESS_KEY] FireProx API Gateway Manager ``` +usage: fire.py [-h] [--profile_name PROFILE_NAME] [--access_key ACCESS_KEY] [--secret_access_key SECRET_ACCESS_KEY] [--session_token SESSION_TOKEN] [--region REGION] [--command COMMAND] [--api_id API_ID] [--url URL] + +FireProx API Gateway Manager + optional arguments: -h, --help show this help message and exit + --profile_name PROFILE_NAME + AWS Profile Name to store/retrieve credentials --access_key ACCESS_KEY AWS Access Key --secret_access_key SECRET_ACCESS_KEY AWS Secret Access Key + --session_token SESSION_TOKEN + AWS Session Token --region REGION AWS Region --command COMMAND Commands: list, create, delete, update --api_id API_ID API ID From 7972499a3074bd1b0d7f897e24581fc2b7e0d9f8 Mon Sep 17 00:00:00 2001 From: Mayk <7395296+yok4i@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:25:45 -0300 Subject: [PATCH 03/10] List APIs from multiple regions --- aws-regions.txt | 17 +++++++++ fire.py | 95 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 aws-regions.txt diff --git a/aws-regions.txt b/aws-regions.txt new file mode 100644 index 0000000..28ddab0 --- /dev/null +++ b/aws-regions.txt @@ -0,0 +1,17 @@ +ap-south-1 +eu-north-1 +eu-west-3 +eu-west-2 +eu-west-1 +ap-northeast-3 +ap-northeast-2 +ap-northeast-1 +ca-central-1 +sa-east-1 +ap-southeast-1 +ap-southeast-2 +eu-central-1 +us-east-1 +us-east-2 +us-west-1 +us-west-2 diff --git a/fire.py b/fire.py index 6c71359..ed71abd 100755 --- a/fire.py +++ b/fire.py @@ -6,13 +6,33 @@ import boto3 import os import sys +import random import datetime import tzlocal import argparse import json import configparser -from typing import Tuple, Callable - +from typing import Tuple, List, Union, Any, Callable + +AWS_DEFAULT_REGIONS = [ + "ap-south-1", + "eu-north-1", + "eu-west-3", + "eu-west-2", + "eu-west-1", + "ap-northeast-3", + "ap-northeast-2", + "ap-northeast-1", + "ca-central-1", + "sa-east-1", + "ap-southeast-1", + "ap-southeast-2", + "eu-central-1", + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2" +] class FireProx(object): def __init__(self, arguments: argparse.Namespace, help_text: str): @@ -76,10 +96,12 @@ def load_creds(self) -> bool: # If profile in files, try it, but flow through if it does not work config_profile_section = f'profile {self.profile_name}' if self.profile_name in credentials: - if config_profile_section not in config: - print(f'Please create a section for {self.profile_name} in your ~/.aws/config file') + if config_profile_section not in config and self.region is None: + print(f'Please create a section for {self.profile_name} in your ~/.aws/config file or provide region') return False - self.region = config[config_profile_section].get('region', 'us-east-1') + # if region is not set, load it from config + if self.region is None: + self.region = config[config_profile_section].get('region', 'us-east-1') try: self.client = boto3.session.Session(profile_name=self.profile_name, region_name=self.region).client('apigateway') @@ -98,7 +120,8 @@ def load_creds(self) -> bool: region_name=self.region ) self.client.get_account() - self.region = self.client._client_config.region_name + if self.region is None: + self.region = self.client._client_config.region_name # Save/overwrite config if profile specified if self.profile_name: if config_profile_section not in config: @@ -298,8 +321,10 @@ def delete_api(self, api_id): return True return False - def list_api(self, deleted_api_id=None): + def list_api(self, deleted_api_id=None, deleting=False): response = self.client.get_rest_apis() + if deleting: + return response['items'] for item in response['items']: try: created_dt = item['createdDate'] @@ -383,18 +408,68 @@ def parse_arguments() -> Tuple[argparse.Namespace, str]: return parser.parse_args(), parser.format_help() +def parse_region(region: str | List, mode: str = "all")-> Union[str, List, None]: + """Parse 'region' and return the final region or set of regions according + to mode + """ + if region is None: + return None + if mode not in ['all', 'random']: + raise ValueError(f"mode should one of ['all', 'random']") + + elif isinstance(region, str): + # if region is a file containing regions, read from it + if os.path.isfile(region): + with open(region) as f: + regions = [reg.strip() for reg in f.readlines()] + if mode == "random": + return random.choice(regions) + elif mode == "all": + return regions + elif ',' in region: + regions = region.split(sep=',') + if mode == "random": + return random.choice(regions) + else: + return regions + else: + return region + + elif isinstance(region, list): + if mode == "all": + return region + elif mode == "random": + return random.choice(region) + + def main(): """Run the main program :return: """ args, help_text = parse_arguments() - fp = FireProx(args, help_text) + #fp = FireProx(args, help_text) if args.command == 'list': - print(f'Listing API\'s...') - result = fp.list_api() + region_parsed = parse_region(args.region) + if isinstance(region_parsed, list): + for region in region_parsed: + args.region = region + fp = FireProx(args, help_text) + print(f'Listing API\'s from {fp.region}...') + result = fp.list_api(deleting=False) + else: + args.region = region_parsed + fp = FireProx(args, help_text) + print(f'Listing API\'s from {fp.region}...') + result = fp.list_api(deleting=False) elif args.command == 'create': + if args.region is not None: + # if region is a file containing regions, choose one randomly + if os.path.isfile(args.region): + with open(args.region) as f: + regions = [reg.strip() for reg in f.readlines()] + self.region = random.choice(regions) result = fp.create_api(fp.url) elif args.command == 'delete': From da0121a464f7d12baedde7716563acf1f051becb Mon Sep 17 00:00:00 2001 From: Mayk <7395296+yok4i@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:29:59 -0300 Subject: [PATCH 04/10] Add list_all command to print all APIs from default regions --- fire.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fire.py b/fire.py index ed71abd..0d7abd9 100755 --- a/fire.py +++ b/fire.py @@ -400,7 +400,7 @@ def parse_arguments() -> Tuple[argparse.Namespace, str]: parser.add_argument('--region', help='AWS Region', type=str, default=None) parser.add_argument('--command', - help='Commands: list, create, delete, update', type=str, default=None) + help='Commands: list, list_all, create, delete, update', type=str, default=None) parser.add_argument('--api_id', help='API ID', type=str, required=False) parser.add_argument('--url', @@ -463,6 +463,13 @@ def main(): print(f'Listing API\'s from {fp.region}...') result = fp.list_api(deleting=False) + elif args.command == "list_all": + for region in AWS_DEFAULT_REGIONS: + args.region = region + fp = FireProx(args, help_text) + print(f'Listing API\'s from {fp.region}...') + result = fp.list_api(deleting=False) + elif args.command == 'create': if args.region is not None: # if region is a file containing regions, choose one randomly From 640f379d79b753714595463ee3c28e9af243ccc6 Mon Sep 17 00:00:00 2001 From: Mayk <7395296+yok4i@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:19:04 -0300 Subject: [PATCH 05/10] Enhance command delete --- fire.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/fire.py b/fire.py index 0d7abd9..4471c2b 100755 --- a/fire.py +++ b/fire.py @@ -4,6 +4,7 @@ import shutil import tldextract import boto3 +import botocore import os import sys import random @@ -13,6 +14,7 @@ import json import configparser from typing import Tuple, List, Union, Any, Callable +from time import sleep AWS_DEFAULT_REGIONS = [ "ap-south-1", @@ -311,15 +313,32 @@ def update_api(self, api_id, url): def delete_api(self, api_id): if not api_id: self.error('Please provide a valid API ID') - items = self.list_api(api_id) - for item in items: - item_api_id = item['id'] - if item_api_id == api_id: + retry = 3 + success = False + error_msg = 'Generic error' + while retry > 0 and not success: + try: response = self.client.delete_rest_api( restApiId=api_id ) - return True - return False + except botocore.exceptions.ClientError as err: + if err.response['Error']['Code'] == 'NotFoundException': + error_msg = 'API not found' + break + elif err.response['Error']['Code'] == 'TooManyRequestsException': + error_msg = 'Too many requests' + sleep(1) + else: + error_msg = err.response['Error']['Message'] + except BaseException as e: + error_msg = 'Generic error' + else: + success = True + error_msg = '' + finally: + retry -= 1 + return success, error_msg + def list_api(self, deleted_api_id=None, deleting=False): response = self.client.get_rest_apis() @@ -400,7 +419,7 @@ def parse_arguments() -> Tuple[argparse.Namespace, str]: parser.add_argument('--region', help='AWS Region', type=str, default=None) parser.add_argument('--command', - help='Commands: list, list_all, create, delete, update', type=str, default=None) + help='Commands: list, list_all, create, delete, prune, update', type=str, default=None) parser.add_argument('--api_id', help='API ID', type=str, required=False) parser.add_argument('--url', @@ -463,6 +482,7 @@ def main(): print(f'Listing API\'s from {fp.region}...') result = fp.list_api(deleting=False) + elif args.command == "list_all": for region in AWS_DEFAULT_REGIONS: args.region = region @@ -470,6 +490,7 @@ def main(): print(f'Listing API\'s from {fp.region}...') result = fp.list_api(deleting=False) + elif args.command == 'create': if args.region is not None: # if region is a file containing regions, choose one randomly @@ -479,10 +500,46 @@ def main(): self.region = random.choice(regions) result = fp.create_api(fp.url) + elif args.command == 'delete': - result = fp.delete_api(fp.api_id) - success = 'Success!' if result else 'Failed!' - print(f'Deleting {fp.api_id} => {success}') + region_parsed = parse_region(args.region) + if region_parsed is None or isinstance(region_parsed, str): + args.region = region_parsed + fp = FireProx(args, help_text) + result, msg = fp.delete_api(fp.api_id) + if result: + print(f'Deleting {fp.api_id} => Success!') + else: + print(f'Deleting {fp.api_id} => Failed! ({msg})') + else: + print(f'[ERROR] More than one region provided for command \'delete\'\n') + sys.exit(1) + + + elif args.command == 'prune': + region_parsed = parse_region(args.region) + if region_parsed is None: + region_parsed = AWS_DEFAULT_REGIONS + if isinstance(region_parsed, str): + region_parsed = [region_parsed] + while True: + choice = input(f"This will delete ALL APIs from region(s): {','.join(region_parsed)}. Proceed? [y/N] ") or 'N' + if choice.upper() in ['Y', 'N']: + break + if choice.upper() == 'Y': + for region in region_parsed: + args.region = region + fp = FireProx(args, help_text) + print(f'Retrieving API\'s from {region}...') + current_apis = fp.list_api(deleting=True) + if len(current_apis) == 0: + print(f'No API found') + else: + for api in current_apis: + result = fp.delete_api(api_id=api['id']) + success = 'Success!' if result else 'Failed!' + print(f'Deleting {api["id"]} => {success}') + elif args.command == 'update': print(f'Updating {fp.api_id} => {fp.url}...') @@ -490,6 +547,7 @@ def main(): success = 'Success!' if result else 'Failed!' print(f'API Update Complete: {success}') + else: print(f'[ERROR] Unsupported command: {args.command}\n') print(help_text) From 841815f434752cbcf3ba797d14ab35e1df27f9ca Mon Sep 17 00:00:00 2001 From: Mayk <7395296+yok4i@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:27:04 -0300 Subject: [PATCH 06/10] Add command 'prune' to delete all APIs for given region or in all regions' --- fire.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fire.py b/fire.py index 4471c2b..17b0730 100755 --- a/fire.py +++ b/fire.py @@ -314,6 +314,7 @@ def delete_api(self, api_id): if not api_id: self.error('Please provide a valid API ID') retry = 3 + sleep_time = 3 success = False error_msg = 'Generic error' while retry > 0 and not success: @@ -327,7 +328,8 @@ def delete_api(self, api_id): break elif err.response['Error']['Code'] == 'TooManyRequestsException': error_msg = 'Too many requests' - sleep(1) + sleep(sleep_time) + sleep_time *= 2 else: error_msg = err.response['Error']['Message'] except BaseException as e: @@ -536,9 +538,11 @@ def main(): print(f'No API found') else: for api in current_apis: - result = fp.delete_api(api_id=api['id']) - success = 'Success!' if result else 'Failed!' - print(f'Deleting {api["id"]} => {success}') + result, msg = fp.delete_api(api_id=api['id']) + if result: + print(f'Deleting {api["id"]} => Success!') + else: + print(f'Deleting {api["id"]} => Failed! ({msg})') elif args.command == 'update': From 4c293ab575d2d4bb6715146ee14502f99def2d92 Mon Sep 17 00:00:00 2001 From: Mayk <7395296+yok4i@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:34:57 -0300 Subject: [PATCH 07/10] Update command 'update' --- fire.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fire.py b/fire.py index 17b0730..1b2af04 100755 --- a/fire.py +++ b/fire.py @@ -546,6 +546,11 @@ def main(): elif args.command == 'update': + region_parsed = parse_region(args.region) + if isinstance(region_parsed, list): + print(f'[ERROR] More than one region provided for command \'update\'\n') + sys.exit(1) + fp = FireProx(args, help_text) print(f'Updating {fp.api_id} => {fp.url}...') result = fp.update_api(fp.api_id, fp.url) success = 'Success!' if result else 'Failed!' From f597fa11a82a12527e94c69015ef6d50ac03f73b Mon Sep 17 00:00:00 2001 From: Mayk <7395296+yok4i@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:41:22 -0300 Subject: [PATCH 08/10] Update command 'create' --- fire.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/fire.py b/fire.py index 1b2af04..94aafee 100755 --- a/fire.py +++ b/fire.py @@ -274,6 +274,7 @@ def create_api(self, url): body=template ) resource_id, proxy_url = self.create_deployment(response['id']) + result = {"id":response['id'],"proxy_url":proxy_url} self.store_api( response['id'], response['name'], @@ -283,6 +284,8 @@ def create_api(self, url): resource_id, proxy_url ) + return result + def update_api(self, api_id, url): if not any([api_id, url]): @@ -469,7 +472,6 @@ def main(): :return: """ args, help_text = parse_arguments() - #fp = FireProx(args, help_text) if args.command == 'list': region_parsed = parse_region(args.region) if isinstance(region_parsed, list): @@ -494,12 +496,9 @@ def main(): elif args.command == 'create': - if args.region is not None: - # if region is a file containing regions, choose one randomly - if os.path.isfile(args.region): - with open(args.region) as f: - regions = [reg.strip() for reg in f.readlines()] - self.region = random.choice(regions) + region_parsed = parse_region(args.region, mode="random") + args.region = region_parsed + fp = FireProx(args, help_text) result = fp.create_api(fp.url) From 010409c3cb282558b38183b035ba3300c5b86911 Mon Sep 17 00:00:00 2001 From: Mayk <7395296+yok4i@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:00:16 -0300 Subject: [PATCH 09/10] Update README --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++--------- fire.py | 2 +- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8958764..4b89598 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Being able to hide or continually rotate the source IP address when making web c ![Black Hills Information Security](https://www.blackhillsinfosec.com/wp-content/uploads/2016/03/BHIS-logo-L-300x300.png "Black Hills Information Security") ## Maintainer -- Follow me on Twitter for more tips, tricks, and tools (or just to say hi)! ([Mike Felch - @ustayready](https://twitter.com/ustayready)) +- Follow me on Twitter for more tips, tricks, and tools (or just to say hi)! ([Mike Felch - @ustayready](https://twitter.com/ustayready)) ### Benefits ## @@ -28,8 +28,8 @@ Being able to hide or continually rotate the source IP address when making web c * All parameters and URI's are passed through * Create, delete, list, or update proxies * Spoof X-Forwarded-For source IP header by requesting with an X-My-X-Forwarded-For header - - + + ### Disclaimers ## * ~~Source IP address is passed to the destination in the X-Forwarded-For header by AWS~~ * ~~($100 to the first person to figure out how to strip it in the AWS config before it reaches the destination LOL!)~~ @@ -39,7 +39,7 @@ Being able to hide or continually rotate the source IP address when making web c * Use of this tool on systems other than those that you own are likely to violate the [AWS Acceptable Use Policy](https://aws.amazon.com/aup/) and could potentially lead to termination or suspension of your AWS account. Further, even use of this tool on systems that you do own, or have explicit permission to perform penetration testing on, is subject to the AWS policy on [penetration testing](https://aws.amazon.com/security/penetration-testing/). ## Credit ## -After releasing FireProx publicly, I learned two others were already using the AWS API Gateway technique. Researching the chain of events and having some great conversations, I came to the realization that the only reason I even knew about it was because of these people. I thought it would be cool to give them a few shout-outs and credit, follow these people -- they are awesome. +After releasing FireProx publicly, I learned two others were already using the AWS API Gateway technique. Researching the chain of events and having some great conversations, I came to the realization that the only reason I even knew about it was because of these people. I thought it would be cool to give them a few shout-outs and credit, follow these people -- they are awesome. Credit goes to [Ryan Hanson - @ryHanson](https://twitter.com/ryHanson) who is the first known source of the API Gateway technique @@ -48,18 +48,19 @@ Shout-out to [Mike Hodges - @rmikehodges](https://twitter.com/rmikehodges) for m Major shout-out, once again, to my good friend [Ralph May - @ralphte1](https://twitter.com/ralphte1) for introducing me to the technique awhile back. ## Basic Usage ## -### Requires AWS access key and secret access key or aws cli configured +##### Requires AWS access key and secret access key or aws cli configured usage: **fire.py** [-h] [--access_key ACCESS_KEY] [--secret_access_key SECRET_ACCESS_KEY] [--region REGION] [--command COMMAND] [--api_id API_ID] [--url URL] FireProx API Gateway Manager ``` -usage: fire.py [-h] [--profile_name PROFILE_NAME] [--access_key ACCESS_KEY] [--secret_access_key SECRET_ACCESS_KEY] [--session_token SESSION_TOKEN] [--region REGION] [--command COMMAND] [--api_id API_ID] [--url URL] +usage: fire.py[-h] [--profile_name PROFILE_NAME] [--access_key ACCESS_KEY] [--secret_access_key SECRET_ACCESS_KEY] [--session_token SESSION_TOKEN] + [--region REGION] [--command COMMAND] [--api_id API_ID] [--url URL] FireProx API Gateway Manager -optional arguments: +options: -h, --help show this help message and exit --profile_name PROFILE_NAME AWS Profile Name to store/retrieve credentials @@ -69,8 +70,8 @@ optional arguments: AWS Secret Access Key --session_token SESSION_TOKEN AWS Session Token - --region REGION AWS Region - --command COMMAND Commands: list, create, delete, update + --region REGION AWS Regions (accepts single region, comma-separated list of regions or file containing regions) + --command COMMAND Commands: list, list_all, create, delete, prune, update --api_id API_ID API ID --url URL URL end-point ``` @@ -78,7 +79,50 @@ optional arguments: * Examples * examples/google.py: Use a FireProx proxy to scrape Google search. * examples/bing.py: Use a FireProx proxy to scrape Bing search. - + +### CLI Usage Examples + +#### List all APIs from default regions using an AWS profile +``` +fire.py --profile_name myprofile --command list_all +``` +Example of output: +``` +Listing API's from ap-south-1... +Listing API's from eu-north-1... +Listing API's from eu-west-3... +Listing API's from eu-west-2... +Listing API's from eu-west-1... +Listing API's from ap-northeast-3... +Listing API's from ap-northeast-2... +Listing API's from ap-northeast-1... +Listing API's from ca-central-1... +Listing API's from sa-east-1... +Listing API's from ap-southeast-1... +Listing API's from ap-southeast-2... +Listing API's from eu-central-1... +Listing API's from us-east-1... +Listing API's from us-east-2... +Listing API's from us-west-1... +Listing API's from us-west-2... +``` + +#### Remove ALL APIs from regions defined in a text file (one per line) +``` +fire.py --command prune --region aws-regions.txt +``` +A confirmation will be required before proceeding: +``` +This will delete ALL APIs from region(s): ap-south-1,eu-north-1,eu-west-3,eu-west-2,eu-west-1, +ap-northeast-3,ap-northeast-2,ap-northeast-1,ca-central-1,sa-east-1,ap-southeast-1, +ap-southeast-2,eu-central-1,us-east-1,us-east-2,us-west-1,us-west-2. Proceed? [y/N] +``` + +#### Create a new API in a random region from a list of regions +``` +fire.py --profile_name myprofile --command create --url https://example.com --region aws-regions.txt +``` + ## Installation ## You can install and run with the following command: diff --git a/fire.py b/fire.py index 94aafee..3b7c7fe 100755 --- a/fire.py +++ b/fire.py @@ -422,7 +422,7 @@ def parse_arguments() -> Tuple[argparse.Namespace, str]: parser.add_argument('--session_token', help='AWS Session Token', type=str, default=None) parser.add_argument('--region', - help='AWS Region', type=str, default=None) + help='AWS Regions (accepts single region, comma-separated list of regions or file containing regions)', type=str, default=None) parser.add_argument('--command', help='Commands: list, list_all, create, delete, prune, update', type=str, default=None) parser.add_argument('--api_id', From 70f8067d8ecb950c87af96ab0aa05f9a72df5991 Mon Sep 17 00:00:00 2001 From: Mayk <7395296+yok4i@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:07:49 -0300 Subject: [PATCH 10/10] Make it more import friendly (based on @chm0dx/fireprox) --- fire.py | 222 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 115 insertions(+), 107 deletions(-) diff --git a/fire.py b/fire.py index 3b7c7fe..1af25ad 100755 --- a/fire.py +++ b/fire.py @@ -16,6 +16,9 @@ from typing import Tuple, List, Union, Any, Callable from time import sleep +class FireProxException(Exception): + pass + AWS_DEFAULT_REGIONS = [ "ap-south-1", "eu-north-1", @@ -37,18 +40,15 @@ ] class FireProx(object): - def __init__(self, arguments: argparse.Namespace, help_text: str): - self.profile_name = arguments.profile_name - self.access_key = arguments.access_key - self.secret_access_key = arguments.secret_access_key - self.session_token = arguments.session_token - self.region = arguments.region - self.command = arguments.command - self.api_id = arguments.api_id - self.url = arguments.url + def __init__(self, **kwargs): + self.profile_name = None + self.access_key = None + self.secret_access_key = None + self.session_token = None + self.region = None self.api_list = [] self.client = None - self.help = help_text + self.__dict__.update(kwargs) if self.access_key and self.secret_access_key: if not self.region: @@ -57,8 +57,6 @@ def __init__(self, arguments: argparse.Namespace, help_text: str): if not self.load_creds(): self.error('Unable to load AWS credentials') - if not self.command: - self.error('Please provide a valid command') def __str__(self): return 'FireProx()' @@ -99,7 +97,7 @@ def load_creds(self) -> bool: config_profile_section = f'profile {self.profile_name}' if self.profile_name in credentials: if config_profile_section not in config and self.region is None: - print(f'Please create a section for {self.profile_name} in your ~/.aws/config file or provide region') + self.error(f'Please create a section for {self.profile_name} in your ~/.aws/config file or provide region') return False # if region is not set, load it from config if self.region is None: @@ -148,11 +146,9 @@ def load_creds(self) -> bool: return False def error(self, error): - print(self.help) - sys.exit(error) + raise FireProxException(error) - def get_template(self): - url = self.url + def get_template(self, url): if url[-1] == '/': url = url[:-1] @@ -264,9 +260,7 @@ def create_api(self, url): if not url: self.error('Please provide a valid URL end-point') - print(f'Creating => {url}...') - - template = self.get_template() + template = self.get_template(url) response = self.client.import_rest_api( parameters={ 'endpointConfigurationTypes': 'REGIONAL' @@ -275,7 +269,7 @@ def create_api(self, url): ) resource_id, proxy_url = self.create_deployment(response['id']) result = {"id":response['id'],"proxy_url":proxy_url} - self.store_api( + return result, self.store_api( response['id'], response['name'], response['createdDate'], @@ -284,7 +278,6 @@ def create_api(self, url): resource_id, proxy_url ) - return result def update_api(self, api_id, url): @@ -296,7 +289,6 @@ def update_api(self, api_id, url): resource_id = self.get_resource(api_id) if resource_id: - print(f'Found resource {resource_id} for {api_id}!') response = self.client.update_integration( restApiId=api_id, resourceId=resource_id, @@ -346,6 +338,7 @@ def delete_api(self, api_id): def list_api(self, deleted_api_id=None, deleting=False): + results = [] response = self.client.get_rest_apis() if deleting: return response['items'] @@ -357,15 +350,15 @@ def list_api(self, deleted_api_id=None, deleting=False): proxy_url = self.get_integration(api_id).replace('{proxy}', '') url = f'https://{api_id}.execute-api.{self.region}.amazonaws.com/fireprox/' if not api_id == deleted_api_id: - print(f'[{created_dt}] ({api_id}) {name}: {url} => {proxy_url}') + results.append(f'[{created_dt}] ({api_id}) {name}: {url} => {proxy_url}') except: pass - return response['items'] + return results def store_api(self, api_id, name, created_dt, version_dt, url, resource_id, proxy_url): - print( + return( f'[{created_dt}] ({api_id}) {name} => {proxy_url} ({url})' ) @@ -472,92 +465,107 @@ def main(): :return: """ args, help_text = parse_arguments() - if args.command == 'list': - region_parsed = parse_region(args.region) - if isinstance(region_parsed, list): - for region in region_parsed: - args.region = region - fp = FireProx(args, help_text) - print(f'Listing API\'s from {fp.region}...') - result = fp.list_api(deleting=False) - else: - args.region = region_parsed - fp = FireProx(args, help_text) - print(f'Listing API\'s from {fp.region}...') - result = fp.list_api(deleting=False) - - - elif args.command == "list_all": - for region in AWS_DEFAULT_REGIONS: - args.region = region - fp = FireProx(args, help_text) - print(f'Listing API\'s from {fp.region}...') - result = fp.list_api(deleting=False) - - - elif args.command == 'create': - region_parsed = parse_region(args.region, mode="random") - args.region = region_parsed - fp = FireProx(args, help_text) - result = fp.create_api(fp.url) + try: + if not args.command: + raise FireProxException('Please provide a valid command') + + if args.command == 'list': + region_parsed = parse_region(args.region) + if isinstance(region_parsed, list): + for region in region_parsed: + args.region = region + fp = FireProx(**vars(args)) + print(f'Listing API\'s from {fp.region}...') + results = fp.list_api(deleting=False) + for result in results: + print(result) + else: + args.region = region_parsed + fp = FireProx(**vars(args)) + print(f'Listing API\'s from {fp.region}...') + results = fp.list_api(deleting=False) + for result in results: + print(result) - elif args.command == 'delete': - region_parsed = parse_region(args.region) - if region_parsed is None or isinstance(region_parsed, str): + elif args.command == "list_all": + for region in AWS_DEFAULT_REGIONS: + args.region = region + fp = FireProx(**vars(args)) + print(f'Listing API\'s from {fp.region}...') + results = fp.list_api(deleting=False) + for result in results: + print(result) + + elif args.command == 'create': + if not args.url: + raise FireProxException('Please provide a valid URL end-point') + region_parsed = parse_region(args.region, mode="random") args.region = region_parsed - fp = FireProx(args, help_text) - result, msg = fp.delete_api(fp.api_id) - if result: - print(f'Deleting {fp.api_id} => Success!') + print(f'Creating => {args.url}...') + fp = FireProx(**vars(args)) + _, result = fp.create_api(args.url) + print(result) + + elif args.command == 'delete': + if not args.api_id: + raise FireProxException('Please provide a valid API id') + region_parsed = parse_region(args.region) + if region_parsed is None or isinstance(region_parsed, str): + args.region = region_parsed + fp = FireProx(**vars(args)) + result, msg = fp.delete_api(args.api_id) + if result: + print(f'Deleting {args.api_id} => Success!') + else: + print(f'Deleting {args.api_id} => Failed! ({msg})') else: - print(f'Deleting {fp.api_id} => Failed! ({msg})') + raise FireProxException(f'[ERROR] More than one region provided for command \'delete\'\n') + + elif args.command == 'prune': + region_parsed = parse_region(args.region) + if region_parsed is None: + region_parsed = AWS_DEFAULT_REGIONS + if isinstance(region_parsed, str): + region_parsed = [region_parsed] + while True: + choice = input(f"This will delete ALL APIs from region(s): {','.join(region_parsed)}. Proceed? [y/N] ") or 'N' + if choice.upper() in ['Y', 'N']: + break + if choice.upper() == 'Y': + for region in region_parsed: + args.region = region + fp = FireProx(**vars(args)) + print(f'Retrieving API\'s from {region}...') + current_apis = fp.list_api(deleting=True) + if len(current_apis) == 0: + print(f'No API found') + else: + for api in current_apis: + result, msg = fp.delete_api(api_id=api['id']) + if result: + print(f'Deleting {api["id"]} => Success!') + else: + print(f'Deleting {api["id"]} => Failed! ({msg})') + + elif args.command == 'update': + if not args.api_id: + raise FireProxException('Please provide a valid API id') + if not args.url: + raise FireProxException('Please provide a valid URL end-point') + region_parsed = parse_region(args.region) + if isinstance(region_parsed, list): + raise FireProxException(f'[ERROR] More than one region provided for command \'update\'\n') + fp = FireProx(**vars(args)) + print(f'Updating {args.api_id} => {args.url}...') + result = fp.update_api(args.api_id, args.url) + success = 'Success!' if result else 'Failed!' + print(f'API Update Complete: {success}') + else: - print(f'[ERROR] More than one region provided for command \'delete\'\n') - sys.exit(1) - - - elif args.command == 'prune': - region_parsed = parse_region(args.region) - if region_parsed is None: - region_parsed = AWS_DEFAULT_REGIONS - if isinstance(region_parsed, str): - region_parsed = [region_parsed] - while True: - choice = input(f"This will delete ALL APIs from region(s): {','.join(region_parsed)}. Proceed? [y/N] ") or 'N' - if choice.upper() in ['Y', 'N']: - break - if choice.upper() == 'Y': - for region in region_parsed: - args.region = region - fp = FireProx(args, help_text) - print(f'Retrieving API\'s from {region}...') - current_apis = fp.list_api(deleting=True) - if len(current_apis) == 0: - print(f'No API found') - else: - for api in current_apis: - result, msg = fp.delete_api(api_id=api['id']) - if result: - print(f'Deleting {api["id"]} => Success!') - else: - print(f'Deleting {api["id"]} => Failed! ({msg})') - - - elif args.command == 'update': - region_parsed = parse_region(args.region) - if isinstance(region_parsed, list): - print(f'[ERROR] More than one region provided for command \'update\'\n') - sys.exit(1) - fp = FireProx(args, help_text) - print(f'Updating {fp.api_id} => {fp.url}...') - result = fp.update_api(fp.api_id, fp.url) - success = 'Success!' if result else 'Failed!' - print(f'API Update Complete: {success}') - - - else: - print(f'[ERROR] Unsupported command: {args.command}\n') + raise FireProxException(f'[ERROR] Unsupported command: {args.command}\n') + + except FireProxException as ex: print(help_text) sys.exit(1)