Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions ectou_metadata/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import datetime
import json
import os
import base64

import boto3.session
import botocore.session
Expand All @@ -19,6 +20,21 @@
_conf_dir = None

_credential_map = {}
_http_authorization = None


def _http_authorization(func):
def wrapper():
if not _http_authorization:
return func()

headers = bottle.request.headers
if 'Authorization' in headers and headers['Authorization'] == _http_authorization:
return func()

return bottle.abort(401, "Unauthorized")

return wrapper


def _lookup_ip_role_arn(source_ip):
Expand Down Expand Up @@ -64,21 +80,25 @@ def _index(items):
@bottle.route("/latest/meta-data/iam")
@bottle.route("/latest/meta-data/iam/security-credentials")
@bottle.route("/latest/meta-data/placement")
@_http_authorization
def slashify():
bottle.redirect(bottle.request.path + "/", 301)


@bottle.route("/")
@_http_authorization
def root():
return _index(["latest"])


@bottle.route("/latest/")
@_http_authorization
def latest():
return _index(["meta-data"])


@bottle.route("/latest/meta-data/")
@_http_authorization
def meta_data():
return _index(["ami-id",
"iam/",
Expand All @@ -91,16 +111,19 @@ def meta_data():


@bottle.route("/latest/meta-data/iam/")
@_http_authorization
def iam():
return _index(["security-credentials/"])


@bottle.route("/latest/meta-data/iam/security-credentials/")
@_http_authorization
def security_credentials():
return _index(["role-name"])


@bottle.route("/latest/meta-data/iam/security-credentials/role-name")
@_http_authorization
def security_credentials_role_name():
role_arn = _get_role_arn()
credentials = _credential_map.get(role_arn)
Expand Down Expand Up @@ -142,58 +165,68 @@ def security_credentials_role_name():


@bottle.route("/latest/meta-data/instance-id")
@_http_authorization
def instance_id():
bottle.response.content_type = 'text/plain'
return "i-deadbeef"


@bottle.route("/latest/meta-data/instance-type")
@_http_authorization
def instance_type():
bottle.response.content_type = 'text/plain'
return "m1.small"


@bottle.route("/latest/meta-data/ami-id")
@_http_authorization
def ami_id():
bottle.response.content_type = 'text/plain'
return "ami-deadbeef"


@bottle.route("/latest/meta-data/local-ipv4")
@_http_authorization
def local_ipv4():
bottle.response.content_type = 'text/plain'
return "127.0.0.1"


@bottle.route("/latest/meta-data/placement/")
@_http_authorization
def placement():
return _index(["availability-zone"])


@bottle.route("/latest/meta-data/placement/availability-zone")
@_http_authorization
def availability_zone():
bottle.response.content_type = 'text/plain'
return "us-east-1x"


@bottle.route("/latest/meta-data/public-hostname")
@_http_authorization
def public_hostname():
bottle.response.content_type = 'text/plain'
return "localhost"


@bottle.route("/latest/meta-data/public-ipv4")
@_http_authorization
def public_ipv4():
bottle.response.content_type = 'text/plain'
return "127.0.0.1"

@bottle.route("/latest/dynamic/instance-identity<slashes:re:/*>")
@_http_authorization
def instance_identity_index(slashes):
bottle.response.content_type = 'text/plain'
return 'document'


@bottle.route("/latest/dynamic/instance-identity<slashes1:re:/+>document<slashes2:re:/*>")
@_http_authorization
def instance_identity_document(slashes1, slashes2):
bottle.response.content_type = 'text/plain'
return '{"region": "us-east-1"}'
Expand All @@ -207,6 +240,8 @@ def main():
parser.add_argument('--port', default=80)
parser.add_argument('--role-arn', help="Default role ARN.")
parser.add_argument('--conf-dir', help="Directory containing configuration files named by source ip.")
parser.add_argument('--auth-file', help=("Require Basic HTTP Authentication, with credentials specified in this"
" file (user:password). See: examples/mitmproxy-ectou-metadata.py"))
args = parser.parse_args()

global _role_arn
Expand All @@ -216,6 +251,12 @@ def main():
_conf_dir = args.conf_dir

app = bottle.default_app()

if args.auth_file:
global _http_authorization
auth_user_password = open(args.auth_file).read().strip()
_http_authorization = "Basic {}".format(base64.b64encode(auth_user_password))

app.run(host=args.host, port=args.port)


Expand Down
24 changes: 24 additions & 0 deletions examples/mitmproxy-ectou-metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# mitmproxy-ectou-metadata.py
#
# Example usage: mitmproxy -s mitmproxy-ectou-metadata.py --ignore-hosts '.+' --proxyauth "$(cat mitmproxy-auth-file.txt)" --listen-host 127.0.0.1 --listen-port 18080
# - will require HTTP Proxy Authentication
# - forwards traffic for 169.254.169.254 to an ectou-metadata instance on 127.0.0.1:18081
# - requests to ectou-metadata will be authenticated using credentials in ectou-auth-file.txt
# - May be used by docker build --build-arg="HTTP_PROXY=..." (see: https://github.com/moby/moby/pull/31584)

import base64
from mitmproxy import http

ectou_metadata_host = "127.0.0.1"
ectou_metadata_port = 18081
ectou_metadata_auth_file = 'ectou-auth-file.txt'

_auth_user_password = open(ectou_metadata_auth_file).read().strip()
_http_authorization = "Basic {}".format(base64.b64encode(_auth_user_password.encode('ascii')).decode('ascii'))


def request(flow):
if flow.request.pretty_host == "169.254.169.254":
flow.request.host = ectou_metadata_host
flow.request.port = ectou_metadata_port
flow.request.headers.set_all("Authorization", [_http_authorization])