Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: amazon

  • Mindful Development and Misunderstandings

    Mindful Development and Misunderstandings

    I consider myself a mindful developer. I strongly feel that morals and ethics and intent are important. In order to continue making the world better, we have to think about the worst thing our code might be used for and prepare for that. This means I spend a lot of time thinking about usage and intent.

    However, like anyone else, I have my blind spots. And it’s amusing to me that I stumbled right over one when it came to an automated submission check.

    Automation

    I made an Amazon Echo skill that tells you when the last queer female died on TV. In November, we noticed it had three negative reviews, all of which misunderstood the purpose of the application. They all thought we were pro-death. Now to me this meant the description of the skill and the output needed to be modified.

    No big deal, right? I’ll just go in and make an edit and I’ll be done.

    Wrong.

    Your skill contains content that violates our content guidelines. You can find our content guidelines here.

    Specifically, any promotion or praise of hate speech, inciting racial or gender hatred, or promotion of groups or organizations which support such beliefs, such as the Ku Klux Klan are not appropriate for the Alexa Skills catalog.

    My change was rejected because it was hate speech.

    No matter how I wrote the description, or phrased the commands, it was getting flagged as hate speech automatically. That’s right, automatically. There was no human to contact, in fact the email said I had to fill in a contact form and maybe I’d get an answer.

    Instead I went to my cohort in computing, who said she was certain it was the phrase “bury your queers.”

    Documentation

    I’ve had a lot of code reviews in my life. I’ve done a lot. There really two aspects of a review that help you survive it:

    1. A robust, if generic, explanation, with specifics outlined.
    2. Clear documentation to help resolve the specifics.

    Amazon doesn’t give either of those.

    There are no specifics that said, outright, “The problem is your content looks like your promoting the death of a specific type of people.” No matter how I tried to rewrite it, was rejected.

    Finally I realized I was going to have to write it all over again, from the ground up, and explain it all better.

    People over Process

    I get grief from plugin developers for being reluctant to specify what is bad and what is good. This probably stems from my upbringing, where I was taught about right and wrong as relative concepts. Is it alright to kill? No. But if the choice is killing a child or yourself, because your car brakes went out, you have to be able to weigh the situation at hand.

    Computers can’t do this. There is no artificial intelligence that can weigh the soul of a person and know if they’re going to do evil. If there was, the face of air travel and politics would be wildly different.

    And the point I make here is that we cannot simply say “this description hits all our buzz words and triggers for badness, therefor it is clearly a bad thing.” It’s just not the case. In restricting us from being able to speak about such horrible things, we give them room to grow in the darkness.

    But that is neither here nor there.

    A More Positive Message

    As Tracy put it, it was a damn good lesson in perspective.

    She didn’t say damn.

    While the “bury your gays” trope is well known to me, it is not universal. And while I feel queer is a perfectly acceptable term that has been in use for a very long time, others do not. People inside and outside our community are often unaware that one of the first pro-gay posters said “queer power” and not “gay power.” It’s just one of those things.

    So the right path is to go forward in a positive way. I’ve rewritten the code to give you more details than just who died. It will now tell you what was posted, how many characters were added, who’s on the air and who’s not.

    And yes, who died. Because that matters too.

  • Housing Large Media Files

    Housing Large Media Files

    For the most part, the WordPress media library is fine. It falls down when we start needing to upload large files, though, for a variety of reasons. When we look at files like large PDFs or movies or podcasts, it’s really not a great solution to upload through WordPress itself. It’s slow, it’s clunky, and worst of all, those large file downloads can slow your site.

    The ‘right’ fix is to offload large media to servers that are built for this sort of thing. And in this case, I’m talking about Amazon AWS or DreamObjects.

    Of course, if you search for solutions like this, you’ll be disappointed. You will mostly find plugins that are geared towards syncing your media library with the cloud services. To be honest, the more I think about doing that, the less I feel like it’s a sustainable idea. Unless the CDN is super fast, it could actually make your site worse off by adding another domain to download from.

    I Don’t Trust Simple CDNs

    I’ve always been skeptical of CDNs in general. When there’s a shared library, it makes sense for everyone to call the same library. That keeps the world in sync. But your own media? The reason a CDN is good is that you can distribute your content across multiple locations. Provided you can actually, you know, do that. And keep them all in sync.

    Before hosting, I worked at a bank, and one of the headaches we had was pushing software updates across multiple servers and locations. After all, you can’t just upgrade the Chicago servers and not the LA and Atlanta ones. Plus you have to do them all at the same time, or make sure Jane in Idaho isn’t in the middle of depositing money when we reboot her server.

    Knowing how crazy all that is, I worry about keeping data in sync across all the servers. What happens when media is updated? Is the CDN built so that my primary location properly triggers updates for everything else, and the data is updated? No matter what, I’m sure I’ll end up with some data out of sync for at least a little while.

    In short, CDN synchronization isn’t simple and anyone who tells me it is, is selling something.

    So Why A CDN At All?

    Big files.

    The goal of a CDN is to speed up delivery of content without slowing down your website. For most images on a website, this isn’t a huge issue. But for those big files, it sure is. And uploading them to the could means three things:

    1. No lost disk space
    2. No lost bandwidth (if someone’s watching a movie for example)
    3. No lost speed (see the aforementioned movie)

    The rest of your CDN ‘needs’ can be handled properly by caching. I prefer server side, but as you like it. This means if I upload my large files to the CDN, I can link directly to them in my post content. Everyone wins.

    Except Uploading Sucks

    The common solution is to manually upload the file via a client like Cyberduck or Transmit, copy the URL, and then paste it into a blog post. Yuck. What I need is a file manager for the cloud. And that doesn’t seem to exist for WordPress.

    So I made something. DreamHost Objects Dropzone lets me upload files to DreamObjects, through WordPress, without touching the file server at all. It’s not perfect. It can be slow when trying to get stats on all the items in a bucket, and I don’t quite have an interface to make it easy to insert links and content into posts. Yet.

    Something to look forward to though.

  • A Fully Functional Alexa Skill

    A Fully Functional Alexa Skill

    I’ve been talking a lot (a lot) about my Amazon Alexa skill.

    First of all, it’s done. It’s published and working. You can check it out on Amazon.com.

    Secondly, a lot of people are like me and need a real, concrete, working example. So I’ve decided to post the entirety of my Amazon Alexa Skill.

    This file, which is actually found on a site in /lwtv-plugin/rest-api/alexa-skills.php consists of two endpoints: the flash briefing and the skill. The skill is called “Bury Your Queers” and while I suspect that part of the code is the least ‘useful’ to people, it’s also good to see the real code.

    Here, then, is the original approved and certified WordPress code for an Alexa Skill:

    The Code

    <?php
    /*
    Description: REST-API - Alexa Skills
    
    For Amazon Alexa Skills
    
    Version: 1.0
    Author: Mika Epstein
    */
    
    if ( ! defined('WPINC' ) ) die;
    
    /**
     * class LWTV_Alexa_Skills
     *
     * The basic constructor class that will set up our JSON API.
     */
    class LWTV_Alexa_Skills {
    
    	/**
    	 * Constructor
    	 */
    	public function __construct() {
    		add_action( 'rest_api_init', array( $this, 'rest_api_init') );
    	}
    
    	/**
    	 * Rest API init
    	 *
    	 * Creates callbacks
    	 *   - /lwtv/v1/flash-briefing
    	 */
    	public function rest_api_init() {
    
    		// Skills
    		register_rest_route( 'lwtv/v1', '/alexa-skills/briefing/', array(
    			'methods' => 'GET',
    			'callback' => array( $this, 'flash_briefing_rest_api_callback' ),
    		) );
    
    		// Skills
    		register_rest_route( 'lwtv/v1', '/alexa-skills/byq/', array(
    			'methods' => [ 'GET', 'POST' ],
    			'callback' => array( $this, 'bury_your_queers_rest_api_callback' ),
    		) );
    
    
    	}
    
    	/**
    	 * Rest API Callback for Flash Briefing
    	 */
    	public function flash_briefing_rest_api_callback( $data ) {
    		$response = $this->flash_briefing();
    		return $response;
    	}
    
    	/**
    	 * Rest API Callback for Bury Your Queers
    	 * This accepts POST data
    	 */
    	public function bury_your_queers_rest_api_callback( WP_REST_Request $request ) {
    
    		$type   = ( isset( $request['request']['type'] ) )? $request['request']['type'] : false;
    		$intent = ( isset( $request['request']['intent']['name'] ) )? $request['request']['intent']['name'] : false;
    		$date   = ( isset( $request['request']['intent']['slots']['Date']['value'] ) )? $request['request']['intent']['slots']['Date']['value'] : false;
    
    		$validate_alexa = $this->alexa_validate_request( $request );
    
    		if ( $validate_alexa['success'] != 1 ) {
    			$error = new WP_REST_Response( array( 'message' => $validate_alexa['message'], 'data' => array( 'status' => 400 ) ) );
    			$error->set_status( 400 );
    			return $error;
    		}
    
    		$response = $this->bury_your_queers( $type, $intent, $date );
    		return $response;
    	}
    
    
    	function alexa_validate_request( $request ) {
    
    		$chain_url = $request->get_header( 'signaturecertchainurl' );
    		$timestamp = $request['request']['timestamp'];
    		$signature = $request->get_header( 'signature' );
    
    	    // Validate that it even came from Amazon ...
    	    if ( !isset( $chain_url ) )
    	    	return array( 'success' => 0, 'message' => 'This request did not come from Amazon.' );
    
    	    // Validate proper format of Amazon provided certificate chain url
    	    $valid_uri = $this->alexa_valid_key_chain_uri( $chain_url );
    	    if ( $valid_uri != 1 )
    	    	return array( 'success' => 0, 'message' => $valid_uri );
    
    	    // Validate certificate signature
    	    $valid_cert = $this->alexa_valid_cert( $request, $chain_url, $signature );
    	    if ( $valid_cert != 1 )
    	    	return array ( 'success' => 0, 'message' => $valid_cert );
    
    	    // Validate time stamp
    		if (time() - strtotime( $timestamp ) > 60)
    			return array ( 'success' => 0, 'message' => 'Timestamp validation failure. Current time: ' . time() . ' vs. Timestamp: ' . $timestamp );
    
    	    return array( 'success' => 1, 'message' => 'Success' );
    	}
    
    	/*
    		Validate certificate chain URL
    	*/
    	function alexa_valid_key_chain_uri( $keychainUri ){
    
    	    $uriParts = parse_url( $keychainUri );
    
    	    if (strcasecmp( $uriParts['host'], 's3.amazonaws.com' ) != 0 )
    	        return ( 'The host for the Certificate provided in the header is invalid' );
    
    	    if (strpos( $uriParts['path'], '/echo.api/' ) !== 0 )
    	        return ( 'The URL path for the Certificate provided in the header is invalid' );
    
    	    if (strcasecmp( $uriParts['scheme'], 'https' ) != 0 )
    	        return ( 'The URL is using an unsupported scheme. Should be https' );
    
    	    if (array_key_exists( 'port', $uriParts ) && $uriParts['port'] != '443' )
    	        return ( 'The URL is using an unsupported https port' );
    
    	    return 1;
    	}
    
    	/*
    	    Validate that the certificate and signature are valid
    	*/
    	function alexa_valid_cert( $request, $chain_url, $signature ) {
    
    		$md5pem     = get_temp_dir() . md5( $chain_url ) . '.pem';
    	    $echoDomain = 'echo-api.amazon.com';
    
    	    // If we haven't received a certificate with this URL before,
    	    // store it as a cached copy
    	    if ( !file_exists( $md5pem ) ) {
    		    file_put_contents( $md5pem, file_get_contents( $chain_url ) );
    		}
    
    	    $pem = file_get_contents( $md5pem );
    
    	    // Validate certificate chain and signature
    	    $ssl_check = openssl_verify( $request->get_body() , base64_decode( $signature ), $pem, 'sha1' );
    
    	    if ($ssl_check != 1 ) {
    		    return( openssl_error_string() );
    		}
    
    	    // Parse certificate for validations below
    	    $parsedCertificate = openssl_x509_parse( $pem );
    	    if ( !$parsedCertificate ) return( 'x509 parsing failed' );
    
    	    // Check that the domain echo-api.amazon.com is present in
    	    // the Subject Alternative Names (SANs) section of the signing certificate
    	    if(strpos( $parsedCertificate['extensions']['subjectAltName'], $echoDomain) === false) {
    	        return( 'subjectAltName Check Failed' );
    	    }
    
    	    // Check that the signing certificate has not expired
    	    // (examine both the Not Before and Not After dates)
    	    $validFrom = $parsedCertificate['validFrom_time_t'];
    	    $validTo   = $parsedCertificate['validTo_time_t'];
    	    $time      = time();
    
    	    if ( !( $validFrom <= $time && $time <= $validTo ) ) {
    	        return( 'certificate expiration check failed' );
    	    }
    
    	    return 1;
    	}
    
    	/**
    	 * Generate the Flash Briefing output
    	 *
    	 * @access public
    	 * @return void
    	 */
    	public function flash_briefing() {
    
    		$query = new WP_Query( array( 'numberposts' => '10' ) );
    		if ( $query->have_posts() ) {
    			while ( $query->have_posts() ) {
    				$query->the_post();
    
    				$response = array(
    					'uid'            => get_the_permalink(),
    					'updateDate'     => get_post_modified_time( 'Y-m-d\TH:i:s.\0\Z' ),
    					'titleText'      => get_the_title(),
    					'mainText'       => get_the_title() . '. ' . get_the_excerpt(),
    					'redirectionUrl' => home_url(),
    				);
    
    				$responses[] = $response;
    			}
    			wp_reset_postdata();
    		}
    
    		if ( count( $responses ) === 1 ) {
    			$responses = $responses[0];
    		}
    
    		return $responses;
    
    	}
    
    	/**
    	 * Generate Bury Your Queers
    	 *
    	 * @access public
    	 * @return void
    	 */
    	public function bury_your_queers( $type = false, $intent = false, $date = false ) {
    
    		$whodied    = '';
    		$endsession = true;
    		$timestamp  = ( strtotime( $date ) == false )? false : strtotime( $date ) ;
    		$helptext   = 'You can find out who died on specific dates by asking me questions like "who died" or "who died today" or "who died on March 3rd" or even "How many died in 2017." If no one died then, I\'ll let you know.';
    
    		if ( $type == 'LaunchRequest' ) {
    			$whodied = 'Welcome to the LezWatch TV Bury Your Queers skill. ' . $helptext;
    			$endsession = false;
    		} else {
    			if ( $intent == 'AMAZON.HelpIntent' ) {
    				$whodied = 'This is the Bury Your Queers skill by LezWatch TV, home of the world\'s greatest database of queer female on TV. ' . $helptext;
    				$endsession = false;
    			} elseif ( $intent == 'AMAZON.StopIntent' || $intent == 'AMAZON.CancelIntent' ) {
    				// Do nothing
    			} elseif ( $intent == 'HowMany' ) {
    				if ( $date == false || $timestamp == false ) {
    					$data     = LWTV_Stats_JSON::statistics( 'death', 'simple' );
    					$whodied  = 'A total of '. $data['characters']['dead'] .' queer female characters have died on TV.';
    				} elseif ( !preg_match( '/^[0-9]{4}$/' , $date ) ) {
    					$whodied    = 'I\'m sorry. I don\'t know how to calculate deaths in anything but years right now. ' . $helptext;
    					$endsession = false;
    				} else {
    					$data     = LWTV_Stats_JSON::statistics( 'death', 'years' );
    					$count    = $data[$date]['count'];
    					$how_many = 'No queer female characters died on TV in ' . $date . '.';
    					if ( $count > 0 ) {
    						$how_many = $count .' queer female ' . _n( 'character', 'characters', $count ) . ' died on TV in ' . $date . '.';
    					}
    					$whodied  = $how_many;
    				}
    			} elseif ( $intent == 'WhoDied' ) {
    				if ( $date == false || $timestamp == false ) {
    					$data    = LWTV_BYQ_JSON::last_death();
    					$name    = $data['name'];
    					$whodied = 'The last queer female to die was '. $name .' on '. date( 'F j, Y', $data['died'] ) .'.';
    				} elseif ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])$/' , $date ) ) {
    					$whodied    = 'I\'m sorry. I don\'t know how to calculate deaths in anything but days right now. ' . $helptext;
    					$endsession = false;
    				} else {
    					$this_day = date('m-d', $timestamp );
    					$data     = LWTV_BYQ_JSON::on_this_day( $this_day );
    					$count    = ( key( $data ) == 'none' )? 0 : count( $data ) ;
    					$how_many = 'No queer females died';
    					$the_dead = '';
    					if ( $count > 0 ) {
    						$how_many  = $count . ' queer female ' . _n( 'character', 'characters', $count ) . ' died';
    						$deadcount = 1;
    						foreach ( $data as $dead_character ) {
    							if ( $deadcount == $count && $count !== 1 ) $the_dead .= 'And ';
    							$the_dead .= $dead_character['name'] . ' in ' . $dead_character['died'] . '. ';
    							$deadcount++;
    						}
    					}
    					$whodied = $how_many . ' on '. date('F jS', $timestamp ) . '. ' . $the_dead;
    				}
    			} else {
    				// We have a weird request...
    				$whodied = 'I\'m sorry, I don\'t understand that request. Please ask me something else.';
    				$endsession = false;
    			}
    		}
    		$response = array(
    			'version'  => '1.0',
    			'response' => array (
    				'outputSpeech' => array (
    					'type' => 'PlainText',
    					'text' => $whodied,
    				),
    				'shouldEndSession' => $endsession,
    			)
    		);
    
    		return $response;
    
    	}
    
    }
    new LWTV_Alexa_Skills();
    

    Some Explanations

    The functions bury_your_queers and bury_your_queers_rest_api_callback are the important ones. The flash briefing is there because I was tired of Amazon being picky about embedded media in RSS feeds.

    The way bury_your_queers_rest_api_callback works is it takes the request data to generate the type of request, the intent, and the date information. Then it passes the full request data to alexa_validate_request which is the part you’ll really want.

    That function, alexa_validate_request, is what’s validating that the request came from Amazon, that it’s got a legit certificate from Amazon, and that the request was made in the last 60 seconds. While all those checks kick back an error, the development tools from Amazon will not show you them. Yet. I’m hoping they will in the future so we can more easily debug, but it was a lot of blind debugging. Not my favorite.

    Some Custom Code

    In the bury_your_queers function, I make some calls to other code not included:

    • LWTV_BYQ_JSON::last_death()
    • LWTV_BYQ_JSON::on_this_day( $this_day )

    Those both reference another rest API class in a different file. What’s important here is not what the data is, but that I’m calling those functions and getting an array back, and using that to fill in my reply. For example, here we have the call for ‘last death’:

    $data    = LWTV_BYQ_JSON::last_death();
    $name    = $data['name'];
    $whodied = 'The last queer female to die was '. $name .' on '. date( 'F j, Y', $data['died'] ) .'.';
    

    From this you can infer that the array kicked back has key for name and died. And in fact, if you look at the JSON output, you’ll see if has that and a bit more. I’m just extracting what is required. The same is true of the other function, LWTV_BYQ_JSON::on_this_day, to which I’m passing a parameter of a date.

  • Custom Alexa Skills and WordPress

    Custom Alexa Skills and WordPress

    Before you start, please note that there is no Amazon Alexa SDK for PHP. And this is a big problem.

    An SDK is a software development kit that helps standardize the development of apps for the software. Using an SDK is basically using a standard library that everyone can access and call and not reinvent the wheel all the bloody time. And Amazon, for whatever reason, has decided that they’d rather push their Lambda hosting, which yes they charge you for, instead of clearly and cleanly document PHP code. Node.js? No problem. PHP? You’re on your own.

    Rant aside, I have now a custom Amazon Skill, self hosted, and powered by WordPress.

    Amazon’s Requirements

    On Monday I showed you how to build a very generic skill. It had no options, and it was actually missing a critical piece. You see, Amazon has six basic requirements to be an app:

    1. The service must be Internet-accessible.
    2. The service must adhere to the Alexa Skills Kit interface.
    3. The service must support HTTP over SSL/TLS, leveraging an Amazon-trusted certificate.
    4. The service must accept requests on port 443.
    5. The service must present a certificate with a subject alternate name that matches the domain name of the endpoint.
    6. The service must validate that incoming requests are coming from Alexa.

    The first five are pretty normal. If it’s not internet accessible, its not going to work. Same with the adherence to the skills kit interface. But that last one was surprisingly difficult and annoying. Mostly because of that lack of a standardized PHP SDK.

    Basically there isn’t a standard way to validate that incoming requests are coming from Alexa, but boy howdy, are there requirements.

    Validating Requests for Alexa

    While it says the requirement is to validate the requests, that’s only one aspect of the game. The three basic parts are these:

    • Verifying that the Request was Sent by Alexa
    • Checking the Signature of the Request
    • Checking the Timestamp of the Request

    And none of those are really well documented for PHP. Thanks.

    The Code

    In Monday’s post, I framed out the majority of the code that will be used. The change will be in this section:

    public function last_post_rest_api_callback( $data ) {
    	$response = $this->last_post();
    	return $response;
    }
    

    It now shows this:

    public function bury_your_queers_rest_api_callback( WP_REST_Request $request ) {
    	$date = ( isset( $request['request']['intent']['slots']['Date']['value'] ) )? $request['request']['intent']['slots']['Date']['value'] : false;
    	$validate_alexa = $this->alexa_validate_request( $request );
    	if ( $validate_alexa['success'] !== 1 ) {
    		$error = new WP_REST_Response( array( 'message' => $validate_alexa['message'], 'data' => array( 'status' => 400 ) ) );
    		$error->set_status( 400 );
    		return $error;
    	}
    	$response = $this->last_post( $date );
    	return $response;
    }
    

    This makes two changes. First it’s grabbing the date from the weirdly stored JSON POST from Alexa and passing it to my last_post function. That code I’m skipping since taking the date, parsing it, and changing your output from last_post is beyond the score. No, I’m going to concentrate on the alexa_validate_request function.

    You should take note of the success check if ( $validate_alexa['success'] !== 1 ) however. You must use a rest response with a 400 because Amazon is very picky.

    alexa_validate_request

    The brunt of the validation is to check if the URL came from Amazon, if the URL is on the certificate chain, if the certificate is legit, and finally if the request was made in the last 60 seconds. Which is a lot to look for.

    In order to write this function, I forked Rich Bowen’s Validate Echo request via PHP code for WordPress. This takes into account some WordPress code that isn’t otherwise available:

    function alexa_validate_request( $request ) {
    	$url            = $request->get_header( 'signaturecertchainurl' );
    	$timestamp      = $request['request']['timestamp'];
    	$signature      = $request->get_header( 'signature' );
    
    	// Validate that it even came from Amazon ...
    	if ( !isset( $url ) )
    		return array( 'success' => 0, 'message' => 'This request did not come from Amazon.' );
    
    	// Validate proper format of Amazon provided certificate chain url
    	$valid_uri = $this->alexa_valid_key_chain_uri( $url );
    	if ( $valid_uri != 1 )
    	    	return array( 'success' => 0, 'message' => $valid_uri );
    
    	// Validate certificate signature
    	$valid_cert = $this->alexa_valid_cert( $request, $signature, $url );
    	if ( $valid_cert != 1 )
    	    	return array ( 'success' => 0, 'message' => $valid_cert );
    
    	// Validate time stamp
    	if (time() - strtotime( $timestamp ) > 60)
    		return array ( 'success' => 0, 'message' => 'Timestamp validation failure. Current time: ' . time() . ' vs. Timestamp: ' . $timestamp );
    
    	// If there was no error, it's a success!
    	return array( 'success' => 1, 'message' => 'Success' );
    }
    

    Within that function, I reference two more: alexa_valid_key_chain_uri and alexa_valid_cert which parse the chain and validate the certificate.

    function alexa_valid_key_chain_uri( $keychainUri ){
    
        $uriParts = parse_url($keychainUri);
    
        if (strcasecmp( $uriParts['host'], 's3.amazonaws.com' ) != 0 )
            return ( 'The host for the Certificate provided in the header is invalid' );
    
        if (strpos( $uriParts['path'], '/echo.api/' ) !== 0 )
            return ( 'The URL path for the Certificate provided in the header is invalid' );
    
        if (strcasecmp( $uriParts['scheme'], 'https' ) != 0 )
            return ( 'The URL is using an unsupported scheme. Should be https' );
    
        if (array_key_exists( 'port', $uriParts ) && $uriParts['port'] != '443' )
            return ( 'The URL is using an unsupported https port' );
    
        return 1;
    }
    
    /*
        Validate that the certificate and signature are valid
    */
    function alexa_valid_cert( $request, $signature, $url ) {
    
        $md5pem     = get_temp_dir() . md5( $url ) . '.pem';
        $echoDomain = 'echo-api.amazon.com';
    
        // If we haven't received a certificate with this URL before,
        // store it as a cached copy
        if ( !file_exists( $md5pem ) ) file_put_contents( $md5pem, file_get_contents( $url ) );
    
        // Validate certificate chain and signature
        $pem = file_get_contents( $md5pem );
        $ssl_check = openssl_verify( $request->get_body(), base64_decode( $signature ), $pem, 'sha1' );
        if ($ssl_check != 1 ) return( openssl_error_string() );
    
        // Parse certificate for validations below
        $parsedCertificate = openssl_x509_parse( $pem );
        if ( !$parsedCertificate ) return( 'x509 parsing failed' );
    
        // Check that the domain echo-api.amazon.com is present in
        // the Subject Alternative Names (SANs) section of the signing certificate
        if(strpos( $parsedCertificate['extensions']['subjectAltName'], $echoDomain) === false) {
            return( 'subjectAltName Check Failed' );
        }
    
        // Check that the signing certificate has not expired
        // (examine both the Not Before and Not After dates)
        $validFrom = $parsedCertificate['validFrom_time_t'];
        $validTo   = $parsedCertificate['validTo_time_t'];
        $time      = time();
        if (!($validFrom <= $time && $time <= $validTo)) {
            return( 'certificate expiration check failed' );
        }
    
        return 1;
    }
    

    A Word About Testing…

    The problem with all this weird code is that the only way to test is to use Amazon’s testing platform and that doesn’t actually throw back errors. The testing environment is fun, because you can type in ‘when was last post’ and it prepends “Alexa, ask HalfElf…” for you. And it shows you exactly what JSON it’s passing to your API and what your API retuned.

    But…

    In the event your API throws an error, you don’t get to see what the error was. No, you get a message saying that the API returned an invalid output.

    Basically the Amazon API has no actual debugging if you’re trying to debug the connection requirements.

    There may have been a lot of swearing involved on my end.

  • Alexa Flash Briefing Skills and Video Enclosures

    Alexa Flash Briefing Skills and Video Enclosures

    One of my goals this year, aided by the inimitable Chris Lema, was to make an Amazon Echo app.

    There’s a lot more to the whole plan, but I want to start with the simple stuff first. So the very first step is that I want to make a “Flash Briefing” app. That will allow people to get the latest posts from my site.

    For the most part, this is trivial. Creating a Flash Briefing Skill is fairly well documented and essentially is this:

    1. Make an account
    2. Create a Skill
    3. Point it to your RSS feed
    4. Give it an icon

    And that works great. Unless, of course, you have videos in a post.

    You see, when I went to add my RSS feed, I got this rather useless error:

    Error: Item [https://example.com/?p=9619] doesn't contain a valid Stream Url. Video Url (if present) must be accompanied with a valid Stream Url in an Item.
    

    What Went Wrong?

    The error was caused by having a video in a post. Now, I need to stress the stupidity here. I have a video inside the post. It’s not a video post, it just has an embedded video because it was contextually needed.

    Logically I googled the error and came up empty. This did not surprise me. I’ve been resigned to learn that Amazon is not actually very helpful with their UX or error messages. I’m not sure why this is but their tech UX, the stuff made for developers not the devices made for end-users, tend to be incredibly poorly designed and ill documented for new people.

    That said, I understood the error was reflecting on a ‘video’ URL, and I had a video in that specific post. I removed the video, tested, and it worked. Ergo the error was caused by the video’s existence. But as it happened, Stream URL had nothing to do with it.

    It Was Elements

    The real issue was found when I read through the feed format details which had mention of a need, for audio content, an “URL specifying the location of audio content for an audio feed.”

    This wasn’t an audio file, but the example for a JSON feed was to include a “streamUrl” value. Oh. And for RSS? An “enclosure element with type attribute set to audio/mpeg”

    This had to be related.

    When I looked at my RSS feed, however, I saw this:

    <enclosure url="https://example.com/path/to/myvideo.mp4" length="3120381" type="video/mp4" />
    

    Wasn’t that what I needed?

    A Second Enclosure

    Apparently the flash briefing RSS code is stupid and thinks that any enclosure has to have the “audio/mpeg” type. So how do I add in this?

    <enclosure url="https://example.com/path/to/myvideo.mp4" length="3120381" type="audio/mpeg" />
    

    By the way yes I reported this to them as a bug. Anyway, the first attempt at fixing this was for me to add a new custom post meta for the enclosure like this:

    
    3120381
    audio/mpeg
    

    That auto-added the proper enclosure code because WordPress knows what it’s doing. Once I was sure that worked, I filed the full bug report and then went the other way.

    Remove The Enclosures

    This is not something I generally recommend. However if you’re not podcasting or vlogging and you have no need to encourage people to download your videos and media via RSS, then you can get away with this:

    function delete_enclosure(){
        return '';
    }
    add_filter( 'do_enclose', 'delete_enclosure' );
    add_filter( 'rss_enclosure', 'delete_enclosure' );
    add_filter( 'atom_enclosure', 'delete_enclosure' );
    

    That removes the enclosure code.

    Build Your Own

    Another fix would have been to make a JSON output or use something like JSONFeed itself. Or of course I could have auto-duplicated the embeds, but that just felt wrong to me.