def fetch_url(table, url_id, base_url): """ * Quick summary of the function * We try to look for the url_id in the dynamodb table to find the corresponding full url * abortions originating in this function * abort with a 500 status code on a read error abort with a 404 status code if the url_id is not an index for a shortened url * abortions originating in functions called from this function * abort with 500 error from get_dynamodb_table * parameters and expected output * :param table: DynamoDb Table object :param url_id: String, a shortened url to be compared against indexes in DynamoDB :param base_url: String, the receiver url of the service. For example, in dev, it would be https://service-shortlink.dev.bgdi.ch/shortlinks/<shortlink_id> :return url: the full url corresponding to the url_id in DynamoDB """ url = None try: response = table.query( IndexName='ShortlinksIndex', KeyConditionExpression=Key('shortlinks_id').eq(url_id)) url = response['Items'][0]['url'] if len( response['Items']) > 0 else None except boto3_exc.Boto3Error as error: # pragma: no cover logger.error( "Internal Error while reading in dynamodb. Error message is %s", str(error)) abort( make_error_msg(500, f'Unexpected internal server error: {str(error)}')) if url is None: logger.error("The Shortlink %s was not found in dynamodb.", url_id) abort(make_error_msg(404, f'This short url doesn\'t exist: {base_url}')) return url
def get(self): if self.conn is None: try: self.conn = boto3.resource('dynamodb', region_name=self.region) except boto3_exc.Boto3Error as error: logger.error( 'internal error during Dynamodb connection init. message is : %s', str(error)) abort(make_error_msg(500, 'Internal error')) return self.conn
def get_dynamodb_table(): table_name = aws_table_name region = aws_region dyn = dynamodb_connection dyn.region = region conn = dyn.get() try: return conn.Table(table_name) except boto3_exc.Boto3Error as error: logger.error( 'DynamoDB error during connection to the table %s. Error message is %s', table_name, str(error)) abort(make_error_msg(500, 'Internal error'))
def get_shortlink(shortlink_id): """ * Quick summary of the function * This route checks the shortened url id and redirect the user to the full url if the redirect parameter is set. If that's not the case, it will return a json containing the informations about the url * Abortions originating in this function * None * Abortions originating in functions called from this function * Abort with a 400 error if the redirect parameter is set to a fantasist value Abort with a 404 error from fetch_url Abort with a 500 error from fetch_url * Parameters and return values * :param shortlink_id: a short url id :return: a redirection to the full url or a json with the full url """ logger.debug("Entry in shortlinks fetch at %f with url_id %s", time.time(), shortlink_id) should_redirect = request.args.get('redirect', 'false') if should_redirect not in ("true", "false"): logger.error("redirect parameter set to a non accepted value : %s", should_redirect) abort( make_error_msg( 400, "accepted values for redirect parameter are true or false.")) logger.debug("Redirection is set to : %s ", str(should_redirect)) table = get_dynamodb_table() url = fetch_url(table, shortlink_id, request.base_url) if should_redirect == 'true': logger.info("redirecting to the following url : %s", url) return redirect(url, code=301) logger.info("fetched the following url : %s", url) response = make_response( jsonify({ 'shorturl': shortlink_id, 'full_url': url, 'success': True })) response.headers = base_response_headers return response
def create_url(table, url): """ * Quick summary of the function * This function creates an id using a magic number based on epoch, then try to write to DynamoDB to save the short url. * abortions originating in this function * abort with a 500 status code on a writing error in DynamoDB * abortions originating in functions called from this function * None raise Exceptions when there is an issue during the dynamo db table write * parameters and expected output * :param table: the aws table on which we will write :param url: the url we want to shorten :return: the shortened url id """ logger.debug("Entry in create_url function") logger.debug(table) try: # we create a magic number based on epoch for our shortened_url id # shortened_url = uuid.uuid5(uuid.NAMESPACE_URL, url).hex shortened_url = f'{int(time.time() * 1000) - 1000000000000}' now = time.localtime() table.put_item( Item={ 'shortlinks_id': shortened_url, 'url': url, 'timestamp': time.strftime('%Y-%m-%d %X', now), 'epoch': str(time.gmtime()) }) logger.info("Exit create_url function with shortened url --> %s", shortened_url) return shortened_url # Those are internal server error: error code 500 except boto3_exc.Boto3Error as error: logger.error( "Internal error while writing in dynamodb. Error message is %s", str(error)) abort(make_error_msg(500, f"Write units exceeded: {str(error)}"))
def create_shortlink(): """ * Quick summary of the function * The create_shortlink route's goal is to take an url and create a shortlink to it. The only parameter we should receive is the url to be shortened, within the post payload. We extract the url to shorten, the request scheme, domain and base path and send them to a function to check those parameters. (check_params) If those parameters are checked, we create the response to be sent to the user and create the shortlink to send back (add_item) * Abortions originating in this function * Abort with a 400 status code if we do not receive a json in the post payloadg Abort with a 403 status code if the Origin header is not set nor one we expect. * Abortions originating in functions called from this function * Abort with a 400 status code from check_params Abort with a 500 status code from add_item * Parameters and return values * :request: the request must contain a Origin Header, and a json payload with an url field :return: a json in response which contains the url which will redirect to the initial url """ logger.debug("Shortlink Creation route entered at %f", time.time()) if request.headers.get('Origin') is None or not \ re.match(allowed_domains_pattern, request.headers['Origin']): logger.critical("Shortlink Error: Invalid Origin. ( %s )", request.headers.get('Origin', 'No origin given')) abort(make_error_msg(403, "Not Allowed")) response_headers = base_response_headers try: url = request.json.get('url', None) except AttributeError as err: logger.error("No Json Received as parameter : %s", err) abort( make_error_msg( 400, "This service requires a json to be posted as a payload.")) except json.decoder.JSONDecodeError: logger.error("Invalid Json Received as parameter") abort( make_error_msg( 400, "The json received was malformed and could not be interpreted as a json." )) scheme = request.scheme domain = request.url_root.replace( scheme, '') # this will return the root url without the scheme base_path = request.script_root logger.debug( "params received are : url: %s, scheme: %s, domain: %s, base_path: %s", url, scheme, domain, base_path) base_response_url = check_params(scheme, domain, url, base_path) table = get_dynamodb_table() response = make_response( jsonify({ "shorturl": ''.join(base_response_url + add_item(table, url)), 'success': True })) response.headers = response_headers response_headers['Access-Control-Allow-Origin'] = request.headers['origin'] response_headers['Access-Control-Allow-Methods'] = 'POST, OPTION' response_headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization,' \ ' x-requested-with, Origin, Accept' logger.info("Shortlink Creation Successful.", extra={"response": json.loads(response.get_data())}) return response
def check_params(scheme, host, url, base_path): """ * Quick summary of the function * In the process to create a shortened url, this is the first step, checking all parameters. If they all fall within expectation, this function return the 'base url' that will lead to the redirection link. On production, that base_url would be http(s)://s.geo.admin.ch/. * Abortions originating in this function * Abort with a 400 status code if there is no url, given to shorten Abort with a 400 status code if the url is over 2046 characters long (dynamodb limitation) Abort with a 400 status code if the application hostname can't be determined Abort with a 400 status code if the application domain is not one of the allowed ones. * Abortions originating in functions called from this function * None * parameters and return values * :param scheme: the scheme (http | https) used to make the query :param host: the hostname of this service :param url: the url given to be shortened :param base_path: the reverse proxy paths in front of this service :return: the base url to reach the redirected url. """ if url is None: logger.error('No url given to shorten, exiting with a bad request') abort(make_error_msg(400, 'url parameter missing from request')) # urls have a maximum size of 2046 character due to a dynamodb limitation if len(url) > 2046: logger.error("Url(%s) given as parameter exceeds characters limit.", url) abort( make_error_msg( 400, f"The url given as parameter was too long. (limit is 2046 " f"characters, {len(url)} given)")) hostname = urlparse(url).hostname if hostname is None: logger.error( "Could not determine hostname from the following url : %s", url) abort(make_error_msg(400, 'Could not determine the query hostname')) domain = ".".join(hostname.split(".")[-2:]) if domain not in allowed_domains and hostname not in allowed_hosts: logger.error( "neither the hostname (%s) nor the domain(%s) are part of their " "respective allowed list of domains (%s) or " "hostnames(%s)", hostname, domain, ', '.join(allowed_domains), ', '.join(allowed_hosts)) abort( make_error_msg( 400, 'Neither Host nor Domain in the url parameter are valid')) if host not in allowed_hosts: """ This allows for compatibility with dev hosts or local builds for testing purpose. """ host = host.replace( '://', '' ) # We make sure here that the :// can't get duplicated in the shorturl base_url = ''.join((scheme, '://', host, base_path if 'localhost' not in host else '')) base_url = ''.join( (base_url, 'v4/shortlink/shortlinks/' if base_url.endswith('/') else '/v4/shortlink/shortlinks/')) else: base_url = ''.join((scheme, '://s.geo.admin.ch/')) return base_url