示例#1
0
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
示例#2
0
 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
示例#3
0
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'))
示例#4
0
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
示例#5
0
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)}"))
示例#6
0
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
示例#7
0
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