Welcome, guest | Sign In | My Account | Store | Cart

Command line prototype to update a Dynamic DNS Service that accepts the GnuDIP protocol (like yi.org):

pydyp.py [-u uname] -w password [-s dipserver] [-p dipserverport] [-d domain]

It shows the power of Twisted framework.

Python, 135 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# 2004 (c) Nicola Paolucci <nick ath durdn doth net>
# OpenSource, Python License

"""
Command line prototype to update a Dynamic DNS Service that
accepts the GnuDIP protocol (like yi.org):

pydyp.py -u <uname> -w <password> -s <dipserver> -p <dipserverport> -d <domain>

"""

import md5
import sys

from twisted.internet import protocol,reactor
from twisted.protocols import basic
from twisted.python import usage

__version__ = '0.4'
__author__=('Nicola Paolucci <nick ath durdn doth net>',)
__thanks_to__=[]
__copyright__=''
__history__="""
  0.1 First prototype version
  0.2 Use of .hexdigest()
  0.3 Refactored to separate mechanism and policy
  0.4 No need to pass the factory to the protocol
""" # -> "


def hashPassword(password, salt):
    p1 = md5.md5(password).hexdigest() + '.' + salt.strip()
    hashedpass = md5.md5(p1).hexdigest()
    return hashedpass

class DIPProtocol(basic.LineReceiver):
    """ Quick implementation of GnuDIP protocol (TCP) as described here:
    http://gnudip2.sourceforge.net/gnudip-www/latest/gnudip/html/protocol.html
    """

    delimiter = '\n'

    def connectionMade(self):
        basic.LineReceiver.connectionMade(self)
        self.expectingSalt = True

    def lineReceived(self, line):
        if self.expectingSalt:
            self.saltReceived(line)
            self.expectingSalt = False
        else:
            self.responseReceived(line)

    def saltReceived(self, salt):
        """Override this."""

    def responseReceived(self, response):
        """Override this."""


class DIPUpdater(DIPProtocol):
    """A quick class to update an IP, then disconnect."""
    def saltReceived(self, salt):
        password = self.factory.getPassword()
        username = self.factory.getUsername()
        domain = self.factory.getDomain()

        msg = '%s:%s:%s:2' % (username, hashPassword(password, salt), domain)
        self.sendLine(msg)

    def responseReceived(self, response):
        code = response.split(':', 1)[0]
        if code == '0':
            pass  # OK
        elif code == '1':
            print 'Authentication failed'
        else:
            print 'Unexpected response from server:', repr(response)

        self.transport.loseConnection()

class DIPClientFactory(protocol.ClientFactory):
     """ Factory used to instantiate DIP protocol instances with
         correct username,password and domain.
     """ # -> "
     protocol = DIPUpdater

     def __init__(self,username,password,domain):
         self.u = username
         self.p = password
         self.d = domain

     def getUsername(self):
         return self.u

     def getPassword(self):
         return self.p

     def getDomain(self):
         return self.d

     def clientConnectionLost(self, connector, reason):
         reactor.stop()

     def clientConnectionFailed(self, connector, reason):
         print 'Connection failed. Reason:', reason


class Options(usage.Options):
     optParameters = [['server', 's','gnudip2.yi.org', 'DIP Server'],
                      ['port', 'p',3495,'DIP Server  port'],
                      ['username', 'u','durdn', 'Username'],
                      ['password', 'w',None,'Password'],
                      ['domain', 'd','durdn.yi.org', 'Domain']]

if __name__ == '__main__':
     config = Options()
     try:
         config.parseOptions()
     except usage.UsageError, errortext:
         print '%s: %s' % (sys.argv[0], errortext)
         print '%s: Try --help for usage details.' % (sys.argv[0])
         sys.exit(1)

     server = config['server']
     port = int(config['port'])
     password = config['password']
     if not password:
         print 'Password not entered. Try --help for usage details.'
         sys.exit(1)

     reactor.connectTCP(server, port,
                        DIPClientFactory(config['username'],password,
                                         config['domain']))
     reactor.run()

I wanted to use a Dynamic DNS Service called yi.org, but did not like the option of installing the suggested small client application to update my IP address on my OpenBSD box. So I resorted to write this really quick script. I put into my crontab to keep my domain always up-to-date with my dynamic IP address at home.

This shows how suited is the Twisted framework to develop networked applications.

6 comments

Mark Rowe 19 years, 9 months ago  # | flag

Simpler MD5.

md5.md5('..').digest().encode('hex')

&darr

md5.md5('..').hexdigest()
Nicola Paolucci (author) 19 years, 9 months ago  # | flag

True, its more concise, I updated the recipe. True, its more concise, I updated the recipe. Thanks for the note, Nick

Andrew Bennetts 19 years, 9 months ago  # | flag

Your dataReceived looks potentially buggy to me. First, a nitpick: you don't need to pass the factory explicitly to the Protocol, or override buildProtocol like that. The default buildProtocol implementation will set the .factory attribute for you.

You seem to be assuming that you only receive exactly one message from the server at a time, and never get partial or multiple messages at once, but there's no guarantee of that (although with such small messages it's unlikely).

At a glance, it appears that perhaps twisted.protocols.basic.LineReceiver would be a better base class DIPProtocol, because it would handle these issues for you, and only trigger the lineReceived method with single, complete lines. (It's not clear from the spec you link to that it is a line-delimited protocol, but I'm guessing it is from your implementation).

Also, your "reqnum" instance variable seems to be misleadingly named; what you really want is a variable to keep track of protocol state, e.g. are you expecting a salt, or expecting a response to a previous request?

Finally, you're mixing mechanism and policy in your DIPProtocol class, which makes it difficult to reuse.

Here's how I'd write that class:

def hashPassword(password, salt):
    p1 = md5.md5(password).hexdigest() + '.' + data.strip()
    return = md5.md5(p1).hexdigest()

class DIPProtocol(basic.LineReceiver):
    """ Quick implementation of GnuDIP protocol (TCP) as described here:
    http://gnudip2.sourceforge.net/gnudip-www/latest/gnudip/html/protocol.html
    """

    delimiter = '\n'

    def connectionMade(self):
        basic.LineReceiver.connectionMade(self)
        self.expectingSalt = True

    def lineReceived(self, line):
        if self.expectingSalt:
            self.saltReceived(line)
            self.expectingSalt = False
        else:
            self.responseReceived(line)

    def saltReceived(self, salt):
        """Override this."""

    def responseReceived(self, response):
        """Override this."""

class DIPUpdater(DIPProtocol):
    """A quick class to update an IP, then disconnect."""
    def saltReceived(self, salt):
        password = self.factory.getPassword()
        username = self.factory.getUsername()
        domain = self.factory.getDomain()

        msg = '%s:%s:%s:2' % (username, hashPassword(password, salt), domain)
        self.sendLine(msg)

    def responseReceived(self, response):
        code = response.split(':', 1)[0]
        if code == '0':
            pass  # OK
        elif code == '1':
            print 'Authentication failed'
        else:
            print 'Unexpected response from server:', repr(response)

        self.transport.loseConnection()

(comment continued...)

Andrew Bennetts 19 years, 9 months ago  # | flag

(...continued from previous comment)

And then make DIPFactory use DIPUpdater instead of DIPProtocol.

A further improvement might be to implement "updateIP" and "removeIP" methods on DIPProtocol, to make subclasses like DIPUpdater even easier to write.

Of course, this level of generalisation might be overkill for the cookbook ;)

Nicola Paolucci (author) 19 years, 9 months ago  # | flag

It's always surprising how open collaboration can improve things fast. It's always surprising how open collaboration can improve things fast.

My recipe was put together "quickly" as the title suggested.

Indeed your refactoring makes it more elegant and reusable.

I integrated it, tested it again, and updated it.

I had to pass the factory to the Updater anyway otherwise it could not find it.

Thanks,

Nick

Nicola Paolucci (author) 19 years, 9 months ago  # | flag

Forget my last remark. Of course passing the factory is not needed... I will update the recipe shortly.