Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port to Python3.7 and pep8 fixes #10

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The Lambda function parses the client update request and performs the update in
You can have multiple `<username>:<password>` combinations, and multiple `<host.example.com>` entries per user. The `dyndns2` protocol uses HTTP basic authentication, so I recommend using randomly generated username/password strings. Note that API Gateway will only respond to HTTPS, so this information is never sent over the internet in the clear.
1. Sign into AWS and navigate to the Lambda Console.
1. Click "Create Lambda Function", and "Skip" selecting a blueprint.
1. Give your function a name (I used `dyndns53_lambda`) and set the runtime to Python 2.7.
1. Give your function a name (I used `dyndns53_lambda`) and set the runtime to Python 3.7.
1. Paste the contents of `dyndns53.py` into the "Lambda function code" box, making sure you have updated your `conf` appropriately.
1. Select the execution role you created above in the "Role" drop-down list; leave "Handler" as `lambda_function.lambda_handler`.
1. Under "Advanced settings", you may wish to increase the timeout from 3 s to 10 s. Calls from Lambda to other AWS services can sometimes be slow.
Expand Down
280 changes: 139 additions & 141 deletions dyndns53.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

from __future__ import print_function


import logging
logger = logging.getLogger(__name__)
Expand All @@ -15,167 +15,165 @@


class AuthorizationMissing(Exception):
status = 401
response = {"WWW-Authenticate":"Basic realm=dyndns53"}
status = 401
response = {"WWW-Authenticate":"Basic realm=dyndns53"}
class HostnameException(Exception):
status = 404
response = "nohost"
status = 404
response = "nohost"
class AuthorizationException(Exception):
status = 403
response = "badauth"
status = 403
response = "badauth"
class FQDNException(Exception):
status = 400
response = "notfqdn"
status = 400
response = "notfqdn"
class BadAgentException(Exception):
status = 400
response = "badagent"
status = 400
response = "badagent"
class AbuseException(Exception):
status = 403
response = "abuse"
status = 403
response = "abuse"


conf = {
'<username>:<password>': {
'hosts': {
'<host.example.com.>': {
'aws_region': 'us-west-2',
'zone_id': '<MY_ZONE_ID>',
'record': {
'ttl': 60,
'type': 'A',
},
'last_update': None,
},
},
},
'<username>:<password>': {
'hosts': {
'<host.example.com.>': {
'aws_region': 'us-west-2',
'zone_id': '<MY_ZONE_ID>',
'record': {
'ttl': 60,
'type': 'A',
},
'last_update': None,
},
},
},
}


re_ip = re.compile(r"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$")
def _parse_ip(ipstring):
m = re_ip.match(ipstring)
if bool(m) and all(map(lambda n: 0 <= int(n) <= 255, m.groups())):
return ipstring
else:
raise BadAgentException("Invalid IP string: {}".format(ipstring))
m = re_ip.match(ipstring)
if bool(m) and all([0 <= int(n) <= 255 for n in m.groups()]):
return ipstring
else:
raise BadAgentException("Invalid IP string: {}".format(ipstring))


client53 = boto3.client('route53','us-west-2')
def r53_upsert(host, hostconf, ip):

record_type = hostconf['record']['type']

record_set = client53.list_resource_record_sets(
HostedZoneId=hostconf['zone_id'],
StartRecordName=host,
StartRecordType=record_type,
MaxItems='1'
)

old_ip = None
if not record_set:
msg = "No existing record found for host {} in zone {}"
logger.info(msg.format(host, hostconf['zone_id']))
else:
record = record_set['ResourceRecordSets'][0]
if record['Name'] == host and record['Type'] == record_type:
if len(record['ResourceRecords']) == 1:
for subrecord in record['ResourceRecords']:
old_ip = subrecord['Value']
else:
msg = "Multiple existing records found for host {} in zone {}"
raise ValueError(msg.format(host, hostconf['zone_id']))
else:
msg = "No existing record found for host {} in zone {}"
logger.info(msg.format(host, hostconf['zone_id']))


if old_ip == ip:
logger.debug("Old IP same as new IP: {}".format(ip))
return False

logger.debug("Old IP was: {}".format(old_ip))
return_status = client53.change_resource_record_sets(
HostedZoneId=hostconf['zone_id'],
ChangeBatch={
'Changes': [
{
'Action': 'UPSERT',
'ResourceRecordSet': {
'Name': host,
'Type': hostconf['record']['type'],
'TTL': hostconf['record']['ttl'],
'ResourceRecords': [
{
'Value': ip
}
]
}
}
]
}
)

return True
record_type = hostconf['record']['type']

record_set = client53.list_resource_record_sets(
HostedZoneId=hostconf['zone_id'],
StartRecordName=host,
StartRecordType=record_type,
MaxItems='1'
)

old_ip = None
if not record_set:
msg = "No existing record found for host {} in zone {}"
logger.info(msg.format(host, hostconf['zone_id']))
else:
record = record_set['ResourceRecordSets'][0]
if record['Name'] == host and record['Type'] == record_type:
if len(record['ResourceRecords']) == 1:
for subrecord in record['ResourceRecords']:
old_ip = subrecord['Value']
else:
msg = "Multiple existing records found for host {} in zone {}"
raise ValueError(msg.format(host, hostconf['zone_id']))
else:
msg = "No existing record found for host {} in zone {}"
logger.info(msg.format(host, hostconf['zone_id']))


if old_ip == ip:
logger.debug("Old IP same as new IP: {}".format(ip))
return False

logger.debug("Old IP was: {}".format(old_ip))
return_status = client53.change_resource_record_sets(
HostedZoneId=hostconf['zone_id'],
ChangeBatch={
'Changes': [
{
'Action': 'UPSERT',
'ResourceRecordSet': {
'Name': host,
'Type': hostconf['record']['type'],
'TTL': hostconf['record']['ttl'],
'ResourceRecords': [
{
'Value': ip
}
]
}
}
]
}
)

return True


def _handler(event, context):

if 'header' not in event:
msg = "Headers not populated properly. Check API Gateway configuration."
raise KeyError(msg)

try:
auth_header = event['header']['Authorization']
except KeyError as e:
raise AuthorizationMissing("Authorization required but not provided.")

try:
auth_user, auth_pass = (
auth_header[len('Basic '):].decode('base64').split(':') )
except Exception as e:
msg = "Malformed basicauth string: {}"
raise BadAgentException(msg.format(event['header']['Authorization']))

auth_string = ':'.join([auth_user,auth_pass])
if auth_string not in conf:
raise AuthorizationException("Bad username/password.")

try:
hosts = set( h if h.endswith('.') else h+'.' for h in
event['querystring']['hostname'].split(',') )
except KeyError as e:
raise BadAgentException("Hostname(s) required but not provided.")

if any(host not in conf[auth_string]['hosts'] for host in hosts):
raise HostnameException()

try:
ip = _parse_ip(event['querystring']['myip'])
logger.debug("User supplied IP address: {}".format(ip))
except KeyError as e:
ip = _parse_ip(event['context']['source-ip'])
msg = "User omitted IP address, using best-guess from $context: {}"
logger.debug(msg.format(ip))

if any(r53_upsert(host,conf[auth_string]['hosts'][host],ip) for host in hosts):
return "good {}".format(ip)
else:
return "nochg {}".format(ip)
if 'header' not in event:
msg = "Headers not populated properly. Check API Gateway configuration."
raise KeyError(msg)

try:
auth_header = event['header']['Authorization']
except KeyError as e:
raise AuthorizationMissing("Authorization required but not provided.")

try:
auth_user, auth_pass = (
auth_header[len('Basic '):].decode('base64').split(':') )
except Exception as e:
msg = "Malformed basicauth string: {}"
raise BadAgentException(msg.format(event['header']['Authorization']))

auth_string = ':'.join([auth_user,auth_pass])
if auth_string not in conf:
raise AuthorizationException("Bad username/password.")

try:
hosts = set( h if h.endswith('.') else h+'.' for h in
event['querystring']['hostname'].split(',') )
except KeyError as e:
raise BadAgentException("Hostname(s) required but not provided.")

if any(host not in conf[auth_string]['hosts'] for host in hosts):
raise HostnameException()

try:
ip = _parse_ip(event['querystring']['myip'])
logger.debug("User supplied IP address: {}".format(ip))
except KeyError as e:
ip = _parse_ip(event['context']['source-ip'])
msg = "User omitted IP address, using best-guess from $context: {}"
logger.debug(msg.format(ip))

if any(r53_upsert(host,conf[auth_string]['hosts'][host],ip) for host in hosts):
return "good {}".format(ip)
else:
return "nochg {}".format(ip)


def lambda_handler(event, context):

try:

response = _handler(event, context)

except Exception as e:
try:
j = {'status':e.status, 'response':e.response, 'additional':e.message}
except AttributeError as f:
j = {'status':500, 'response':"911", 'additional':str(e)}
finally:
raise type(e), type(e)(json.dumps(j)), sys.exc_info()[2]

return { 'status': 200, 'response': response }
try:
response = _handler(event, context)
except Exception as e:
try:
error_info = {'status':e.status, 'response':e.response, 'additional':e.message}
ssalonen marked this conversation as resolved.
Show resolved Hide resolved
except AttributeError:
# Fallback to more simple error description
error_info = {'status':500, 'response':"911", 'additional':str(e)}
finally:
raise type(e)(json.dumps(error_info)) from e

return { 'status': 200, 'response': response }