don't_panic
personal and professional blog of mike pultz, technology specialist and serial entrepreneur.

25Mar/123

How to Make Images Look Good on the IPad 3

The new Apple IPad (IPad 3) came out on the 16th, and probably the first thing I noticed about it is how great the screen looks. It's armed with same "retina" display that the IPhone 4 came out with a few years ago. It's great! Except for one thing- a lot of images on the web look like crap now!

(This first image is the standard resolution, the second is optimized for IPhone 4 and IPad 3 screens)

    

Now- this isn't new. The IPhone 4 has the same "quirk", but I think it's less noticeable given the the size of it's screen. It really stands out on the IPad 3's 9.7 inch screen.

The Problem with Pixels

With the advent of high pixel density displays, the pixel itself is now a relative unit.

According to the CSS 2.1 Spec:

Pixel units are relative to the resolution of the viewing device, i.e., most often a computer display. If the pixel density of the output device is very different from that of a typical computer display, the user agent should rescale pixel values.

So, a CSS "pixel" indicates one point on the "virtual" pixel grid to which your CSS design aligns. This either directly matches the actual device, or it is "somehow" scaled to suite.

Talking about the new IPad 3 specifically, the new retina display has a huge 2048 x 1536 pixel resolution- double what most sites are designed for. On a desktop machine, if you doubled your screen resolution, websites would just show up half as big. But on the IPad 3, it stretches the site so it "fills up" the screen. The problem with this is stretching raster images (gif, png, jpeg's) can make them look really distorted and full of artifacts.

So- how do you fix this?

The easiest way to fix this is to make a second copy of all your images at double the resolution, and then use these versions when visitors are on an IPad or IPhone (or any device that has a higher pixel density).

Fixing it in CSS

The min-device-pixel-ratio media query can be used to target style for high pixel density displays. For the moment vendor prefixes are required, until there is a standard format. For example, Mozilla and Webkit prefixes work the same way, but Opera requires the pixel ratio as a fraction.

-moz-min-device-pixel-ratio: 2
-o-min-device-pixel-ratio: 2/1
-webkit-min-device-pixel-ratio: 2
min-device-pixel-ratio: 2

Right now, we only care about IPhone/IPad, so we'll use the -webkit-min-device-pixel-ratio tag.

So let's say you had a single class, loading the 300 x 300 px image:

.logo {
 background-image: url(cat_300.jpg);
 width: 300px;
 height: 300px;
}

You would then create a second copy of the image at 600 x 600 px resolution, and add this in your CSS:

@media only screen and (-webkit-min-device-pixel-ratio: 2) {
 .logo {
   background-image: url(cat_600.jpg);
   background-size: 300px 300px;
 }
}

This loads the 600 x 600 px image, but forces the background-size to 300 x 300 px when the device is a webkit device, and the pixel ratio is 2.

Forcing the 600 x 600 px image into a 300 x 300 px box, forces the image to a pixel density of 2.

Fixing Inline Images

So that's CSS- what about plain old <img> tags?

You can use the window.devicePixelRatio property in JavaScript to determine if the pixel density of the screen is > 1 and if so, cycle through all the images on the page and change their image src to the higher resolution image.

An easy way to do this is to add a class to all the images you want to replace. In this case, I've added the "hd" class to the image tags.

<img src="cat_300.jpg" width="300" height="300" class="hd" />

Then add some simple JavaScript to update the tags. In my example, I've used jQuery just to make things easier, and simply did a text replace in the src image name, changing the "300" to a 600". The width/height of the <img> tag needs to stay at 300 x 300 px, forcing the pixel density of 2.

$(document).ready(function()
{
    if ( (window.devicePixelRatio) && (window.devicePixelRatio >= 2) )
    {
        var images = $('img.hd');        

        for(var i=0; i<images.length; i++)
        {
            images.eq(i).attr('src', images.eq(i).attr('src').replace('300', '600'))
        }
    }
});

Now, there are all sorts of ways to do this- this is just one example.

The other way to do this is to just always load the higher resolution images. The only downside, is those higher resolution images are likely almost twice the size of their originals- so loading them only when required will save bandwidth.

What about SVG?

SVG (Scalable Vector Graphics) is also another great way to handle this. SVG files are actually XML files that have instructions on how to "draw" the image on a canvas, rather than using a static raster image. SVG files can scale to different sizes/pixel densities, without distorting.

The only downsides with SVG, is that sometimes the file size for complex images are actually a lot bigger than their raster counterparts, and browser support is still incomplete- so if you care about your site working in Internet Explorer, then you still need to have some raster images and do conditional loading.

Parting Thoughts

Remember- you don't *actually* need to do any of this; a lot of images will still look "fine" being stretched.

But if you want your site to look it's best, it's worth spending the time to optimize it for higher pixel density devices- there's going to be no shortage of them in the coming years!

8Jan/122

How To Mine Twitter Streams from PHP in Real Time

Need to mine Twitter for tweets related to certain keywords?

No problem-

Twitter provides a pretty simple streaming interface to the onslaught of tweets it receives, letting you specify whatever keywords you want to search for, in a real-time "live" way.

To do this, I created a simple PHP class that can run in the background, collecting tweets for certain keywords:

ctwitter_stream.php

class ctwitter_stream
{
    private $m_username;
    private $m_password;

    public function __construct()
    {
        //
        // set a time limit to unlimited
        //
        set_time_limit(0);
    }

    //
    // set the login details
    //
    public function login($_username, $_password)
    {
        $this->m_username = $_username;
        $this->m_password = $_password;
    }

    //
    // process a tweet object from the stream
    //
    private function process_tweet(array $_data)
    {
        print_r($_data);

        return true;
    }

    //
    // the main stream manager
    //
    public function start(array $_keywords)
    {
        while(1)
        {
            $fp = fsockopen("ssl://stream.twitter.com", 443, $errno, $errstr, 30);
            if (!$fp)
            {
                echo "ERROR: Twitter Stream Error: failed to open socket";
            } else
            {
                //
                // build the request
                //
                $request  = "GET /1/statuses/filter.json?track=";
                $request .= urlencode(implode($_keywords, ',')) . " HTTP/1.1\r\n";
                $request .= "Host: stream.twitter.com\r\n";
                $request .= "Authorization: Basic ";
                $request .= base64_encode($this->m_username . ':' . $this->m_password);
                $request .= "\r\n\r\n";

                //
                // write the request
                //
                fwrite($fp, $request);

                //
                // set it to non-blocking
                //
                stream_set_blocking($fp, 0);

                while(!feof($fp))
                {
                    $read   = array($fp);
                    $write  = null;
                    $except = null;

                    //
                    // select, waiting up to 10 minutes for a tweet; if we don't get one, then
                    // then reconnect, because it's possible something went wrong.
                    //
                    $res = stream_select($read, $write, $except, 600, 0);
                    if ( ($res == false) || ($res == 0) )
                    {
                        break;
                    }

                    //
                    // read the JSON object from the socket
                    //
                    $json = fgets($fp);
                    if ( ($json !== false) && (strlen($json) > 0) )
                    {
                        //
                        // decode the socket to a PHP array
                        //
                        $data = json_decode($json, true);
                        if ($data)
                        {
                            //
                            // process it
                            //
                            $this->process_tweet($data);
                        }
                    }
                }
            }

            fclose($fp);
            sleep(10);
        }

        return;
    }
};

The "process_tweet()" method will be called for each matching tweet- just modify that method to process the tweet however you want (load it into a database, print it to screen, email it, etc). The keyword matching isn't perfect- if you search for a string of words, it won't necessarily match the words in that exact order, but you can check that yourself from the process_tweet() method.

Then create a simple PHP application to run the collector:

require 'ctwitter_stream.php';

$t = new ctwitter_stream();

$t->login('your twitter username', 'your twitter password');

$t->start(array('facebook', 'fbook', 'fb'));

Just provide your twitter account username/password, and then an array of keywords/strings to search for.

Since this application runs continuously in the background, it's obviously not meant to be run via a web request, but meant to be run from the command line of your Unix or Windows box.

According to the Twitter documentation, the default access level allows up to 400 keywords, so you can track all sorts of things at the same time. If you need more details about the Twitter streaming API, it's available here.

This class uses the HTTPS PHP stream- so you'll need the OpenSSL extension enabled for it to work.

16May/100

Automatic Dial Resource Fail-over in Asterisk

Asterisk is generally pretty reliable, but termination providers aren't always so good; in a market where anybody can re-sell an upstream provider, or setup a few Asterisk boxes and start routing calls for people, it's generally a good idea to have a "backup" provider (or three) to route your calls through.

You can easily setup an Asterisk system, to fail-over to secondary systems, if your primary provider fails for some reason- and this can all be done right in the dial plan, using a simple MACRO.

Add this MACRO to your dial plan:

[macro-direct-dial]
exten => s,1,Set(CALL_ATTEMPT=1)
exten => s,2,Set(TERM_PROVIDER=${TERM_PROVIDER1})
exten => s,3,Dial(${TERM_PROVIDER}/${ARG1},60)
exten => s,4,GotoIf($["${CALL_ATTEMPT}" >= "${MAX_PROVIDERS}"]?s-CANCEL,1)
exten => s,5,Set(CALL_ATTEMPT=$[${CALL_ATTEMPT} + 1])
exten => s,6,Goto(s-${DIALSTATUS},1)

exten => s-BUSY,1,Noop()
exten => s-NOANSWER,1,Noop()
exten => s-CANCEL,1,Hangup()
exten => s-HANGUP,1,Hangup()

exten => s-CHANUNAVAIL,1,Set(TERM_PROVIDER=${EVAL(${TERM_PROVIDER${CALL_ATTEMPT}})})
exten => s-CHANUNAVAIL,2,Goto(s,3)

exten => s-CONGESTION,1,Set(TERM_PROVIDER=${EVAL(${TERM_PROVIDER${CALL_ATTEMPT}})})
exten => s-CONGESTION,2,Goto(s,3)

Now you'll need to route your calls into this MACRO; this can vary by dial plan, as you may have a special configuration for different area codes, or country codes, or based on some least-cost-routing business decisions, but a simple example would be something like this:

[default]
exten => _1NXXNXXXXXX,1,Answer()
exten => _1NXXNXXXXXX,2,Macro(direct-dial,${EXTEN})
exten => _1NXXNXXXXXX,3,Hangup()

This routes any NANPA numbers through the direct-dial MACRO above, passing in the phone number as the first argument to the MACRO.

Now, before this will work, you'll need to configure some variables; this can be done in many places- in my working configuration, I have these variables dynamically generated via an AGI script, based on the phone number being dialed. This way I can control dial-groups, by phone number, based on a cost/preference/etc.

In this example, we'll simply set these values in the globals section of the extensions.conf file:

[globals]
TERM_PROVIDER1 = SIP/first_provider
TERM_PROVIDER2 = IAX/second_provider
TERM_PROVIDER3 = SIP/last_provider
MAX_PROVIDERS = 3

So I've configured three fictitious termination providers; you can specify as many as you like, as long as the TERM_PROVIDER increments one for each, and you set the MAX_PROVIDERS value to the total number of providers listed.

This is obviously more useful if this list is automatically generated somehow, or changed based on the phone number being dialed, otherwise the retries could simply be hard-coded into the dial plan.

Now when you dial your number, it will start with the first (default) provider; if the dial() function returns a congestion or channel un-available error, the MACRO will cycle to the next provider, until it as gone through all of the providers listed.

12Feb/1032

Using DKIM in Exim

Since Exim 4.70, DKIM (DomainKeys Indentified Mail - RFC4871) has been supported by default.

The current implementation supports signing outgoing mail, as well as verifying signatures in incoming messages, using the acl_smtp_dkim ACL. By default, DKIM signatures are verified as new messages come in, though no action is taken unless you've implicitly configured rules in the DKIM ACL.

After installing Exim (>= 4.70), you should see debug logs for incoming mail from servers that have DKIM signatures setup- they look like:

DKIM: d=gmail.com s=gamma c=relaxed/relaxed a=rsa-sha256 [verification succeeded]
Verifying Incoming Mail

By default, Exim does not filter any mail based on the validity of the DKIM signature- it's up to you to add ACL rules to control what happens when you receive messages with "bad" signatures.

First add an ACL section for the DKIM processing; this should be included with your other ACL statements:

acl_smtp_dkim = acl_check_dkim

Next, after the "begin acl", section, add your DKIM ACL section, and by default, accept all messages in this ACL:

acl_check_dkim:

	accept

Now you need to decide what kind of rules you want to setup- you probably don't want to put a rule that applies to all domains- though, if the company went to the trouble of adding DKIM signatures to their e-mail, you'd hope they'd get it right, and not publish invalid public keys.

For now, let's add a simple rule for gmail; google knows what they're doing, so their systems should be setup correctly:

acl_check_dkim:

	#
	# check the DKIM signature for gmail
	#
	deny 	message 	= Common guys, what's going on?
		sender_domains 	= gmail.com
		dkim_signers 	= gmail.com
		dkim_status 	= none:invalid:fail

	accept

You can add as many rules, for whatever domains you want in this ACL.

Signing Outgoing Mail

Now that you're checking incoming mail, you probably want to sign mail coming out of your system. This is a relatively easy process, that I've broken down into three steps:

Step1- Generate a private and public key to sign your messages; you can do this easily with openssl:

#openssl genrsa -out dkim.private.key 768

Then extract the public key from the private key:

#openssl rsa -in dkim.private.key -out dkim.public.key -pubout -outform PEM

Step2- Configure the Exim remote-smtp transport to sign outgoing messages, using your new private key. You'll need to pick a domain and a selector for this process.

When remote SMTP servers validate your DKIM signatures, they simply do a DNS look up, based on the selector and your domain- the domain needs to (obviously) be a valid domain you own, that you can add DNS entries to, and the selector can be any string you want. So, for example, using the domain "example.com", and the selector "x", you would add to the remote_smtp transport in Exim:

remote_smtp:
        driver = smtp
        dkim_domain = example.com
        dkim_selector = x
        dkim_private_key = dkim.private.key
        dkim_canon = relaxed

This tells Exim to sign any outbound e-mail, using the domain example.com, the selector "x", and the private key we just generated. The dkim_canon = relaxed, sets the canonicalization method to use when signing messages. DKIM supports "simple" and "relaxed" algorithms- to understand the difference, see section 3.4 of the DKIM RFC.

Step3- add your DKIM public key to your DNS.

The DKIM public key generated above is advertised to other SMTP servers, using a DNS TXT record. In DNS for the domain example.com, add a new TXT record:

x._domainkey.example.com.   TXT v=DKIM1; t=y; k=rsa; p=<public key>

Where "x" is the selector you used above, and <public key> is the public key data (minus the key header/footer text).

When setup correctly, your DKIM text record should look something like this:

# host -t txt x._domainkey.example.com

x._domainkey.example.com descriptive text "v=DKIM1\; t=y\; k=rsa\; p=MIGfMA0GCS
qGSIb3DQEBAQUAA4GNADCBiQKBgQC5k8yUyuyu9UAVHHU7Al4ppTDtxFWsZ6Pqd9NWZnomtewBdz8I
2LJkqmA/3Cyb5Eiaqk4NulPFfDbfA0Lkw7SNyOS9BRN02KGtKIWjFqDwjB99haaWYw9H4IZcuJp0Y
q0kySCdBp/sPP+iTotdBiE85Jakw3tzgYkdvaS05ZUdBwIDAQAB"

(lines breaks were added for readability- your entry should be one continuous line)

This DNS record is referred to as the "selector" record; you need to also setup a "policy" record. The policy record is your domains policy for domain keys- you should start with something like:

_domainkey.example.com. t=y; o=~;

The t=y specifies that you are in test mode and this should be removed when you are certain that your domain key setup is functioning properly. The "~" in the o=~ specifies that some of the mail from your domain is signed, but not all. You could also specify o=- if all of the mail coming from your domain will be signed.

Once you have all of that in-place,  restart Exim, and send out a message using the remote-smtp transport. You should now see a DKIM-Signature: header listed in the message headers, which lists your domain (as d=), and selector (as s=), as well as a signature for this e-mail, which can be validated against your public DKIM key, that you've published in DNS.

For more information, see the Exim DKIM page, or the DKIM RFC.

UPDATE:

Once you've set everything up, you can test your DKIM (and SPF and SenderID, etc) install, by using the port25.com validation service.

Just send an e-mail to check-auth@verifier.port25.com, and it will auto-respond with a validation report

30Apr/091

Handling SIP URI Dialing in Asterisk

Asterisk, by design, is very "extension" orientated- that is, if you want to dial an end-point, it requires an extension to route the call to. These extensions (defined in the asterisk extensions.conf file), can be extensions registered to phones, DIDs (XXXYYYZZZZ), or simply usernames assigned to users by the network administrator. Extensions are used for both incoming, and outgoing phone calls.asterisk

For example, if I place a call through my SIP phone to 1-444-555-1212, then asterisk will look up the "extension" 14445551212 in the extensions.conf file, to determine how to route the call.

Similarly to how e-mail works, an artifact of these extensions is a direct SIP address, which is basically your SIP extension @ your SIP server- so, if my phone was extension 555, and my SIP server had the IP address 192.168.1.1, then my SIP address would be: sip:555@192.168.1.1, and (if it's not implictely blocked), I can be dialed directly using that SIP address.

But, because Asterisk is so extension orientated, it doesn't easily allow for outbound dialing, using remote SIP addresses; If I try to dial the address sip:444@somedomain.com, Asterisk will immediately strip off the host portion (@somedomain.com), and try to route the call based simply on the extension "444"- which, since it's an extension on a remote server (@somedomain.com), it won't be able to route it locally, and will fail.

The solution? Well, it's not ideal, but Asterisk provides the ability to use wild cards in the extensions.conf file (it refers to them as "patterns") when doing extension look ups; this is handy when you have blocks of extension or DID's- you can use the wildcard to map like extensions to the same config, keeping your config file small.

The issue, is that the wild card only compares against the local part of the SIP URI, which can look like almost anything, including other phone numbers.

First, define some general config

[general]
;
; Your termination provider (defined in sip.conf)
;
TERM_PROVIDER = SIP/company_peer

;
; The IP address of this asterisk server
;
ASTERISK_IP = 192.168.1.1

The "ASTERISK_IP" address is important later, as we'll use it to validate outgoing SIP addresses.

Then in your default config, you should have something like this configured already- handling NANPA style dialing, and international dialing

[default]
;
; Dial NANPA style phone numbers directly
;
exten => _1NXXNXXXXXX,n,Dial(${TERM_PROVIDER}/${EXTEN},60)
exten => _1NXXNXXXXXX,n,HangUp()

;
; Dial international numbers directly
;
exten => _011.,n,Dial(${TERM_PROVIDER}/${EXTEN}, 60)
exten => _011.,n,HangUp()

After all the specific matches are done, then add:

;
; very last, assume anything else is a SIP URI
;
exten => _.,n,GotoIf($[${SIPDOMAIN} = ${ASTERISK_IP}]?unhandled)
exten => _.,n,GotoIf($[${SIPDOMAIN} = ${ASTERISK_IP}:5060]?unhandled)
exten => _.,n,Macro(uri-dial,${EXTEN}@${SIPDOMAIN})
exten => _.,n,HangUp()

;
; if the call doesn't match anything
;
[unhandled]
exten => s,n,Congestion()

The reason this has to be last, is because matching the extension "_." will match *anything*- basically it's a catch-all. You're saying that if it doesn't match anything else before this, then assume it must be a SIP URI.

This section also compares the ${SIPDOMAIN} variable to your ASTERISK_IP address; this ensures that only SIP URI's with remote hosts are processed as SIP URI's. If the host matches our ASTERISK_IP address value (ie- it's a local extension), then it should have already matched something above this catch-all config.

Pass any SIP URI dials to the uri-dial MACRO, merging back together the extension and the SIP domain value.

;
; handle dialing SIP uri's directly
;
[macro-uri-dial]
exten => s,n,NoOp(Calling as SIP address: ${ARG1})
exten => s,n,Dial(SIP/${ARG1},60)

This solution works, but isn't ideal, as it will match anything that didn't already match; a better solution would be for it to NOT strip off the SIP domain, and allow for using a regular expression of some kind to check the extension, but there is currently no better way of handling this in Asterisk.