Just An Application

November 3, 2013

Service Discovery In Android And iOS: Part Four – Doing It Properly In Java

Filed under: DNS, DNS Based Service Discovery, mDNS — Tags: , , , — Simon Lewis @ 8:37 pm

To do mDNS based service discovery in Java we would need to be able to

  • send and receive UDP multicast packets
  • read and write DNS messages

Fortunately both of theses things are eminently doable.

1.0 Multicasting

UDP multicasting we get for free courtesy of the class java.net.MulticastSocket.

1.1 Creating A Multicast Socket

Creating a multicast socket is just like creating a unicast socket but in addition we must call the method

    public void joinGroup(InetAddress mcastaddr)
                throws
                    IOException

to join the multicast group we wish to send to, e.g.,

    ...

    socket = new MulticastSocket();
    group  = InetAddress.getByName(MDNS_GROUP_ADDRESS);
    socket.joinGroup(group);
    
    ...

where MDNS_GROUP_ADDRESS is defined as

    private static final String MDNS_GROUP_ADDRESS = "224.0.0.251";

1.2 Sending

Sending a datagram is also the same as in the unicast case, e.g.,

    ...
    
    packet.setAddress(group);
    packet.setPort(MDNS_PORT);
    socket.send(packet);

    ...

where packet is an instance of java.net.DatagramPacket, group is the IV we initialized when creating the socket and MDNS_PORT isdefined as

    private static final int MDNS_PORT = 5353;

1.3 Receiving

Receiving on a multicast socket is exactly the same as on a unicast socket

2.0 Reading A DNS Message

The class MessageReader is responsible for reading incoming DNS messages. Only responses are handled as that is all we expect to see.

The components of the response are handed to an instance of ResponseHandler as they are read.

These are the methods that read the message.

    ...
    
    public void read(ResponseHandler theHandler)
                throws
                    Exception
    {
        int id        = readUnsignedShort();
        int bitfields = readUnsignedShort();
    
        if ((bitfields & QR_RESPONSE_BIT) == 0)
        {
            // query !
        }
        else
        if ((bitfields & RCODE_BITS) == 0)
        {
            theHandler.beginResponse();
    
            int qdcount = readUnsignedShort();
            int ancount = readUnsignedShort();
            int nscount = readUnsignedShort();
            int arcount = readUnsignedShort();
    
            readQuestions(qdcount, theHandler);
            readRecords(Category.ANSWER, ancount, theHandler);
            readRecords(Category.AUTHORITY, nscount, theHandler);
            readRecords(Category.ADDITIONAL, arcount, theHandler);
            if (offset != end)
            {
                throw new Exception();
            }
            theHandler.endResponse();
        }
        else
        {
            // error !
        }
    }
	
    private void readQuestions(int theCount, ResponseHandler theHandler)
                 throws
                     Exception
    {
        for (int i = 0; i < theCount; i++)
        {
            theHandler.question(
                           readNodeName(),
                           TYPE.get(readUnsignedShort()),
                           CLASS.get(readUnsignedShort()));
        }
    }
	
    private void readRecords(Category theCategory, int theCount, ResponseHandler theHandler)
                 throws
                     Exception
    {
        for (int i = 0; i < theCount; i++)
        {
            readRecord(theCategory, theHandler);
        }
    }
	
    private void readRecord(Category theCategory, ResponseHandler theHandler)
                 throws
                     Exception
    {
        String name     = readNodeName();
        int    type     = readUnsignedShort();
        int    klass    = readUnsignedShort();
        int    ttl      = readInt();
        int    rdlength = readUnsignedShort();
    
        switch (type)
        {
            case DNS.T_A:
    
                theHandler.a(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_AAAA:
    
                theHandler.aaaa(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_PTR:
    
                theHandler.ptr(theCategory, name, ttl, readNodeName());
                break;
    
            case DNS.T_TXT:
    
                theHandler.txt(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_SRV:
    
                theHandler.srv(
                               theCategory,
                               name,
                               ttl,
                               readUnsignedShort(),
                               readUnsignedShort(),
                               readUnsignedShort(),
                               readNodeName());
                break;
    
            default:
    
                theHandler.rr(theCategory, name, type, klass, ttl, makeRDATA(rdlength));
        }
    }
    
    ...

3.0 Writing a DNS Message

This is left as an exercise for the reader.

4.0 A Very Simple API

To do DNS base service discovery we need the ability to make queries and see the responses.

The class xper.net.mdns.Client defines the method

    public void query(String theName, TYPE theType)
                throws
                    Exception

which can be used to make queries.

An instance of the Client class can be obtained by calling the method

    public static Client getClient(ResponseHandler theHandler)

where the ResponseHandler interface is defined as follows

    public interface ResponseHandler
    {
        public enum Category
        {
            ANSWER,
            AUTHORITY,
            ADDITIONAL,
        }
	
        //
	
        public void beginResponse();
	
        //
	
        public void question(String theName, TYPE theType, CLASS theClass);
	
        //
	
        public void a(Category theCategory, String name, int ttl, RDATA makeRDATA);
	
        public void aaaa(Category theCategory, String name, int ttl, RDATA makeRDATA);
    
        public void ptr(Category theCategory, String theName, int theTTL, String thePtr);
	
        public void srv(
                        Category theCategory, 
                        String   theName, 
                        int      theTTL, 
                        int      thePriority, 
                        int      theWeight, 
                        int      thePort, 
                        String   theTarget);
	
        public void txt(Category theCategory, String theName, int theTTL, RDATA theData);
	
        //
	
        public void rr(Category theCategory, String theName, int theType, int theClass, int theTTL, RDATA theData);
    
        //
	
        public void endResponse();
    
    }

5.0 The FindServices Class

The FindServices class uses the ‘API’ to find services of a given type.

    // FindServices.java
    
    // Copyright (c) 2013 By Simon Lewis. All Rights Reserved.
    
    package xper.net.sd;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import android.util.Log;
    
    import xper.net.dns.core.CLASS;
    import xper.net.dns.core.RDATA;
    import xper.net.dns.core.ResponseHandler;
    import xper.net.dns.core.TYPE;
    import xper.net.mdns.Client;
    
    public final class FindServices
                       implements
                           ResponseHandler
    {
        public FindServices(String theType, ServiceListener theListener)
        {
            client   = Client.getClient(this);
            type     = theType;
            listener = theListener;
            services = new HashMap<String, ServiceData>();
        }
        
        //
        
        public void start()
                    throws
                        Exception
        {
            log("start");
            client.query(type, TYPE.PTR);
        }
        
        //
        
        @Override
        public void beginResponse()
        {
            log(">> beginResponse");
        }
        
        @Override
        public void question(String theName, TYPE theType, CLASS theClass)
        {
            log("question: " + theName + " " + theType + " " + theClass);
        }
        
        @Override
        public void a(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " A: " + theName);
        }
        
        @Override
        public void aaaa(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " AAAA: " + theName);
        }
        
        @Override
        public void ptr(Category theCategory, String theName, int theTTL, String thePtr)
        {
            log(theCategory + " PTR: " + thePtr);
		
            switch (theCategory)
            {
                case ANSWER:
        
                    ptr(theName, thePtr);
                    break;
        
                default:
        
                    ;
            }
        }
        
        @Override
        public void srv(
                        Category theCategory, 
                        String   theName, 
                        int      theTTL, 
                        int      thePriority, 
                        int      theWeight, 
                        int      thePort, 
                        String   theTarget)
        {
            log(theCategory + " SRV: " + theName);
		
            switch (theCategory)
            {
                case ANSWER:
                case ADDITIONAL:
        
                    srv(theName, thePriority, theWeight, thePort, theTarget);
                    break;
        
                default:
        
                    ;
            }
        }
        
        @Override
        public void txt(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " TXT: " + theName);
		
            switch (theCategory)
            {
                case ANSWER:
                case ADDITIONAL:
        
                    txt(theName, theData);
                    break;
        
                default:
        
                    // Ignore
                    ;
            }
        }
        
        @Override
        public void rr(Category theCategory, String theName, int theType, int theClass, int theTTL, RDATA theData)
        {
            log(theCategory + " rr: " + theName + " type == " + theType + " class == " + theClass);
        }
        
        @Override
        public void endResponse()
        {
            for (ServiceData s: services.values())
            {
                try
                {
                    if (!s.haveSRV)
                    {
                        client.query(s.name, TYPE.SRV);
                    }
                    else
                    if (!s.haveTXT)
                    {
                        client.query(s.name, TYPE.TXT);
                    }
                    else
                    if (!s.found)
                    {
                        log("Found service " + s.name);
                        s.found = true;
                        listener.serviceFound(s);
                    }
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
            log("<< endResponse");
        }
            
        //
            
        private void ptr(String theName, String theServiceName)
        {
            ServiceData service    = getService(theServiceName);
            
            if (service == null)
            {
                addService(theServiceName);
            }
            else
            {
                // ????
            }
        }
            
        private void srv(String theName, int thePriority, int theWeight, int thePort, String theTarget)
        {
            ServiceData service = getService(theName);
            
            if ((service != null) && !service.haveSRV)
            {
                log("priority == " + thePriority);
                log("weight   == " + theWeight);
                log("port     == " + thePort);
                log("target   == " + theTarget);
			
                service.target   = theTarget;
                service.port     = thePort;
                service.priority = thePriority;
                service.weight   = theWeight;
                service.haveSRV  = true;
            }
            else
            {
                log("Unsolicited SRV record for " + theName + " !");
            }
        }
            
        private void txt(String theName, RDATA theData)
        {
            ServiceData service = getService(theName);
            
            if ((service != null) && !service.haveTXT)
            {
                try
                {
                    service.keyValuePairs = makeKeyValuePairs(theData.asStrings());
                    service.haveTXT       = true;
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
            else
            {
                log("Unsolicited TXT record for " + theName + " !");
            }
        }
            
        //
            
        private Map<String, String> makeKeyValuePairs(List theRawStrings)
        {
            Map<String, String> keyValuePairs = new HashMap<String, String>();
            
            for (String s : theRawStrings)
            {
                String key   = null;
                String value = null;
                int    index = s.indexOf('=');
			
                if (index != -1)
                {
                    key   = s.substring(0, index);
                    value = s.substring(index + 1);
                }
                else
                {
                    key   = s;
                    value = "";
                }
                log("key == " + key + " value == " + value);
                if (keyValuePairs.get(key) == null)
                {
                    keyValuePairs.put(key, value);
                }
            }
            return keyValuePairs;
        }
            
        //
            
        private ServiceData getService(String theName)
        {
            return services.get(theName);
        }
            
        private void addService(String theName)
        {
            services.put(theName, new ServiceData(theName, type));
        }
            
        private void log(String theString)
        {
            Log.d("FindServices", theString);
        }
            
        //
            
        private Client                   client;
        private String                   type;
        private ServiceListener          listener;
        private Map<String, ServiceData> services;
            
            
        private static final class ServiceData
                                   implements
                                       Service
        {
            @Override
            public String getName()
            {
                return name;
            }
            
            @Override
            public String getType()
            {
                return type;
            }
            
            @Override
            public String getTarget() 
            {
                return target;
            }
            
            @Override
            public int getPort() 
            {
                return port;
            }
            
            @Override
            public int getPriority() 
            {
                return priority;
            }
            
            @Override
            public int getWeight() 
            {
                return weight;
            }
            
            @Override
            public Map<String, String> getKeyValuePairs()
            {
                return keyValuePairs;
            }
            
            ServiceData(String theName, String theType) 
            {
                name = theName;
                type = theType;
            }
            
            //
            
            private String              name;
            private String              type;
            private String              target;
            private int                 port;
            private int                 priority;
            private int                 weight;
            private Map<String, String> keyValuePairs;
            
            //
            
            private boolean             haveSRV;
            private boolean             haveTXT;
            private boolean             found;
        }
    }

6.0 Examples

The following examples all show the output from an instance of the FindServices class initialized with the service type

    _ipp._tcp.local.

All output is the result of running the output of adb logcat through the command grep FindServices

All the examples involve combinations of three hosts on a wifi network.

6.1 Example One

An Android device and a actual printer.

It takes three queries to obtain all the necessary information


    D/FindServices( 2549): start
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2549): ANSWER PTR: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): << endResponse
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: Canon MG6200 series._ipp._tcp.local. SRV IN
    D/FindServices( 2549): ANSWER SRV: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): priority == 0
    D/FindServices( 2549): weight   == 0
    D/FindServices( 2549): port     == 631
    D/FindServices( 2549): target   == 7D300C000000.local.
    D/FindServices( 2549): << endResponse
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: Canon MG6200 series._ipp._tcp.local. TXT IN
    D/FindServices( 2549): ANSWER TXT: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): key == txtvers value == 1
    D/FindServices( 2549): key == rp value == ipp/printer
    D/FindServices( 2549): key == note value ==
    D/FindServices( 2549): key == qtotal value == 1
    D/FindServices( 2549): key == priority value == 15
    D/FindServices( 2549): key == ty value == Canon MG6200 series
    D/FindServices( 2549): key == product value == (Canon MG6200 series)
    D/FindServices( 2549): key == pdl value == application/octet-stream,image/urf,image/jpeg
    D/FindServices( 2549): key == adminurl value == http://7D300C000000.local.
    D/FindServices( 2549): key == usb_MFG value == Canon
    D/FindServices( 2549): key == usb_MDL value == MG6200 series
    D/FindServices( 2549): key == usb_CMD value == URF
    D/FindServices( 2549): key == UUID value == 00000000-0000-1000-8000-8887177D300C
    D/FindServices( 2549): key == URF value == CP1,PQ4-5,RS600,SRGB24,W8,DM3,OB9,OFU0
    D/FindServices( 2549): key == Color value == T
    D/FindServices( 2549): key == Duplex value == T
    D/FindServices( 2549): key == Scan value == T
    D/FindServices( 2549): key == mac value == 88:87:17:7D:30:0C
    D/FindServices( 2549): Found service Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): << endResponse

6.2 Example Two

An Android device and a Mac.

The Mac is running an instance of the CUPS 1.7.0 test server ippserver with the name ipp_server_1 is which listening on port 6363.

The MacOS X mDNS implementation, mDNSResponder, is following the recommendation in RFC 6763 and is returning the relevant SRV and TXT records as well as the AAAA and A records as additional records in the response to the original query.


    D/FindServices( 2815): start
    D/FindServices( 2815): >> beginResponse
    D/FindServices( 2815): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2815): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): priority == 0
    D/FindServices( 2815): weight   == 0
    D/FindServices( 2815): port     == 6363
    D/FindServices( 2815): target   == Simons-Computer.local.
    D/FindServices( 2815): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): key == rp value == ipp/print
    D/FindServices( 2815): key == ty value == Test Printer
    D/FindServices( 2815): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 2815): key == product value == (Printer)
    D/FindServices( 2815): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2815): key == Color value == F
    D/FindServices( 2815): key == Duplex value == F
    D/FindServices( 2815): key == usb_MFG value == Test
    D/FindServices( 2815): key == usb_MDL value == Printer
    D/FindServices( 2815): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 2815): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 2815): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): << endResponse

6.3 Example Three

An Android device and a Mac.

The Mac is running two instances of ippserver.

As in Example Two all the necessary information is contained in the reply to the original query.

        
    D/FindServices( 2929): start
    D/FindServices( 2929): >> beginResponse
    D/FindServices( 2929): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2929): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): ANSWER PTR: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): priority == 0
    D/FindServices( 2929): weight   == 0
    D/FindServices( 2929): port     == 6363
    D/FindServices( 2929): target   == Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): key == rp value == ipp/print
    D/FindServices( 2929): key == ty value == Test Printer
    D/FindServices( 2929): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 2929): key == product value == (Printer)
    D/FindServices( 2929): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2929): key == Color value == F
    D/FindServices( 2929): key == Duplex value == F
    D/FindServices( 2929): key == usb_MFG value == Test
    D/FindServices( 2929): key == usb_MDL value == Printer
    D/FindServices( 2929): ADDITIONAL SRV: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): priority == 0
    D/FindServices( 2929): weight   == 0
    D/FindServices( 2929): port     == 6364
    D/FindServices( 2929): target   == Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL TXT: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): key == rp value == ipp/print
    D/FindServices( 2929): key == ty value == Test Printer
    D/FindServices( 2929): key == adminurl value == http://Simons-Computer.local:6364/
    D/FindServices( 2929): key == product value == (Printer)
    D/FindServices( 2929): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2929): key == Color value == F
    D/FindServices( 2929): key == Duplex value == F
    D/FindServices( 2929): key == usb_MFG value == Test
    D/FindServices( 2929): key == usb_MDL value == Printer
    D/FindServices( 2929): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 2929): Found service ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): << endResponse
    

6.4 Example Four

An Android device, a printer and a Mac running an instance of ippserver.

Just to show it can be done.


    D/FindServices( 3453): start
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: _ipp._tcp.local. PTR IN
    D/FindServices( 3453): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): priority == 0
    D/FindServices( 3453): weight   == 0
    D/FindServices( 3453): port     == 6363
    D/FindServices( 3453): target   == Simons-Computer.local.
    D/FindServices( 3453): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): key == rp value == ipp/print
    D/FindServices( 3453): key == ty value == Test Printer
    D/FindServices( 3453): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 3453): key == product value == (Printer)
    D/FindServices( 3453): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 3453): key == Color value == F
    D/FindServices( 3453): key == Duplex value == F
    D/FindServices( 3453): key == usb_MFG value == Test
    D/FindServices( 3453): key == usb_MDL value == Printer
    D/FindServices( 3453): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 3453): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 3453): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: _ipp._tcp.local. PTR IN
    D/FindServices( 3453): ANSWER PTR: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: Canon MG6200 series._ipp._tcp.local. SRV IN
    D/FindServices( 3453): ANSWER SRV: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): priority == 0
    D/FindServices( 3453): weight   == 0
    D/FindServices( 3453): port     == 631
    D/FindServices( 3453): target   == 7D300C000000.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: Canon MG6200 series._ipp._tcp.local. TXT IN
    D/FindServices( 3453): ANSWER TXT: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): key == txtvers value == 1
    D/FindServices( 3453): key == rp value == ipp/printer
    D/FindServices( 3453): key == note value ==
    D/FindServices( 3453): key == qtotal value == 1
    D/FindServices( 3453): key == priority value == 15
    D/FindServices( 3453): key == ty value == Canon MG6200 series
    D/FindServices( 3453): key == product value == (Canon MG6200 series)
    D/FindServices( 3453): key == pdl value == application/octet-stream,image/urf,image/jpeg
    D/FindServices( 3453): key == adminurl value == http://7D300C000000.local.
    D/FindServices( 3453): key == usb_MFG value == Canon
    D/FindServices( 3453): key == usb_MDL value == MG6200 series
    D/FindServices( 3453): key == usb_CMD value == URF
    D/FindServices( 3453): key == UUID value == 00000000-0000-1000-8000-8887177D300C
    D/FindServices( 3453): key == URF value == CP1,PQ4-5,RS600,SRGB24,W8,DM3,OB9,OFU0
    D/FindServices( 3453): key == Color value == T
    D/FindServices( 3453): key == Duplex value == T
    D/FindServices( 3453): key == Scan value == T
    D/FindServices( 3453): key == mac value == 88:87:17:7D:30:0C
    D/FindServices( 3453): Found service Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): << endResponse


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

Advertisements

2 Comments »

  1. […] To make things slightly simpler we can wrap up the classes and their delegates in a single class FindServices as we did in the Java case. […]

    Pingback by Service Discovery In Android And iOS: Part Five – iOS Take One | Just An Application — November 12, 2013 @ 12:16 pm

  2. […] And if none of the previous choices are to your liking then there is also the theoretical possibility of doing the whole thing from scratch as in the Java case. […]

    Pingback by Service Discovery In Android And iOS: Part Nine – iOS Take Five | Just An Application — November 22, 2013 @ 12:51 am


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: