| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
""" |
|---|
| 6 |
Dict client protocol implementation. |
|---|
| 7 |
|
|---|
| 8 |
@author: Pavel Pergamenshchik |
|---|
| 9 |
""" |
|---|
| 10 |
|
|---|
| 11 |
from twisted.protocols import basic |
|---|
| 12 |
from twisted.internet import defer, protocol |
|---|
| 13 |
from twisted.python import log |
|---|
| 14 |
from StringIO import StringIO |
|---|
| 15 |
|
|---|
| 16 |
def parseParam(line): |
|---|
| 17 |
"""Chew one dqstring or atom from beginning of line and return (param, remaningline)""" |
|---|
| 18 |
if line == '': |
|---|
| 19 |
return (None, '') |
|---|
| 20 |
elif line[0] != '"': |
|---|
| 21 |
mode = 1 |
|---|
| 22 |
else: |
|---|
| 23 |
mode = 2 |
|---|
| 24 |
res = "" |
|---|
| 25 |
io = StringIO(line) |
|---|
| 26 |
if mode == 2: |
|---|
| 27 |
io.read(1) |
|---|
| 28 |
while 1: |
|---|
| 29 |
a = io.read(1) |
|---|
| 30 |
if a == '"': |
|---|
| 31 |
if mode == 2: |
|---|
| 32 |
io.read(1) |
|---|
| 33 |
return (res, io.read()) |
|---|
| 34 |
elif a == '\\': |
|---|
| 35 |
a = io.read(1) |
|---|
| 36 |
if a == '': |
|---|
| 37 |
return (None, line) |
|---|
| 38 |
elif a == '': |
|---|
| 39 |
if mode == 1: |
|---|
| 40 |
return (res, io.read()) |
|---|
| 41 |
else: |
|---|
| 42 |
return (None, line) |
|---|
| 43 |
elif a == ' ': |
|---|
| 44 |
if mode == 1: |
|---|
| 45 |
return (res, io.read()) |
|---|
| 46 |
res += a |
|---|
| 47 |
|
|---|
| 48 |
def makeAtom(line): |
|---|
| 49 |
"""Munch a string into an 'atom'""" |
|---|
| 50 |
|
|---|
| 51 |
return filter(lambda x: not (x in map(chr, range(33)+[34, 39, 92])), line) |
|---|
| 52 |
|
|---|
| 53 |
def makeWord(s): |
|---|
| 54 |
mustquote = range(33)+[34, 39, 92] |
|---|
| 55 |
result = [] |
|---|
| 56 |
for c in s: |
|---|
| 57 |
if ord(c) in mustquote: |
|---|
| 58 |
result.append("\\") |
|---|
| 59 |
result.append(c) |
|---|
| 60 |
s = "".join(result) |
|---|
| 61 |
return s |
|---|
| 62 |
|
|---|
| 63 |
def parseText(line): |
|---|
| 64 |
if len(line) == 1 and line == '.': |
|---|
| 65 |
return None |
|---|
| 66 |
else: |
|---|
| 67 |
if len(line) > 1 and line[0:2] == '..': |
|---|
| 68 |
line = line[1:] |
|---|
| 69 |
return line |
|---|
| 70 |
|
|---|
| 71 |
class Definition: |
|---|
| 72 |
"""A word definition""" |
|---|
| 73 |
def __init__(self, name, db, dbdesc, text): |
|---|
| 74 |
self.name = name |
|---|
| 75 |
self.db = db |
|---|
| 76 |
self.dbdesc = dbdesc |
|---|
| 77 |
self.text = text |
|---|
| 78 |
|
|---|
| 79 |
class DictClient(basic.LineReceiver): |
|---|
| 80 |
"""dict (RFC2229) client""" |
|---|
| 81 |
|
|---|
| 82 |
data = None |
|---|
| 83 |
MAX_LENGTH = 1024 |
|---|
| 84 |
state = None |
|---|
| 85 |
mode = None |
|---|
| 86 |
result = None |
|---|
| 87 |
factory = None |
|---|
| 88 |
|
|---|
| 89 |
def __init__(self): |
|---|
| 90 |
self.data = None |
|---|
| 91 |
self.result = None |
|---|
| 92 |
|
|---|
| 93 |
def connectionMade(self): |
|---|
| 94 |
self.state = "conn" |
|---|
| 95 |
self.mode = "command" |
|---|
| 96 |
|
|---|
| 97 |
def sendLine(self, line): |
|---|
| 98 |
"""Throw up if the line is longer than 1022 characters""" |
|---|
| 99 |
if len(line) > self.MAX_LENGTH - 2: |
|---|
| 100 |
raise ValueError("DictClient tried to send a too long line") |
|---|
| 101 |
basic.LineReceiver.sendLine(self, line) |
|---|
| 102 |
|
|---|
| 103 |
def lineReceived(self, line): |
|---|
| 104 |
try: |
|---|
| 105 |
line = line.decode("UTF-8") |
|---|
| 106 |
except UnicodeError: |
|---|
| 107 |
return |
|---|
| 108 |
if self.mode == "text": |
|---|
| 109 |
code = "text" |
|---|
| 110 |
else: |
|---|
| 111 |
if len(line) < 4: |
|---|
| 112 |
log.msg("DictClient got invalid line from server -- %s" % line) |
|---|
| 113 |
self.protocolError("Invalid line from server") |
|---|
| 114 |
self.transport.LoseConnection() |
|---|
| 115 |
return |
|---|
| 116 |
code = int(line[:3]) |
|---|
| 117 |
line = line[4:] |
|---|
| 118 |
method = getattr(self, 'dictCode_%s_%s' % (code, self.state), self.dictCode_default) |
|---|
| 119 |
method(line) |
|---|
| 120 |
|
|---|
| 121 |
def dictCode_default(self, line): |
|---|
| 122 |
"""Unkown message""" |
|---|
| 123 |
log.msg("DictClient got unexpected message from server -- %s" % line) |
|---|
| 124 |
self.protocolError("Unexpected server message") |
|---|
| 125 |
self.transport.loseConnection() |
|---|
| 126 |
|
|---|
| 127 |
def dictCode_221_ready(self, line): |
|---|
| 128 |
"""We are about to get kicked off, do nothing""" |
|---|
| 129 |
pass |
|---|
| 130 |
|
|---|
| 131 |
def dictCode_220_conn(self, line): |
|---|
| 132 |
"""Greeting message""" |
|---|
| 133 |
self.state = "ready" |
|---|
| 134 |
self.dictConnected() |
|---|
| 135 |
|
|---|
| 136 |
def dictCode_530_conn(self): |
|---|
| 137 |
self.protocolError("Access denied") |
|---|
| 138 |
self.transport.loseConnection() |
|---|
| 139 |
|
|---|
| 140 |
def dictCode_420_conn(self): |
|---|
| 141 |
self.protocolError("Server temporarily unavailable") |
|---|
| 142 |
self.transport.loseConnection() |
|---|
| 143 |
|
|---|
| 144 |
def dictCode_421_conn(self): |
|---|
| 145 |
self.protocolError("Server shutting down at operator request") |
|---|
| 146 |
self.transport.loseConnection() |
|---|
| 147 |
|
|---|
| 148 |
def sendDefine(self, database, word): |
|---|
| 149 |
"""Send a dict DEFINE command""" |
|---|
| 150 |
assert self.state == "ready", "DictClient.sendDefine called when not in ready state" |
|---|
| 151 |
self.result = None |
|---|
| 152 |
self.data = None |
|---|
| 153 |
self.state = "define" |
|---|
| 154 |
command = "DEFINE %s %s" % (makeAtom(database.encode("UTF-8")), makeWord(word.encode("UTF-8"))) |
|---|
| 155 |
self.sendLine(command) |
|---|
| 156 |
|
|---|
| 157 |
def sendMatch(self, database, strategy, word): |
|---|
| 158 |
"""Send a dict MATCH command""" |
|---|
| 159 |
assert self.state == "ready", "DictClient.sendMatch called when not in ready state" |
|---|
| 160 |
self.result = None |
|---|
| 161 |
self.data = None |
|---|
| 162 |
self.state = "match" |
|---|
| 163 |
command = "MATCH %s %s %s" % (makeAtom(database), makeAtom(strategy), makeAtom(word)) |
|---|
| 164 |
self.sendLine(command.encode("UTF-8")) |
|---|
| 165 |
|
|---|
| 166 |
def dictCode_550_define(self, line): |
|---|
| 167 |
"""Invalid database""" |
|---|
| 168 |
self.mode = "ready" |
|---|
| 169 |
self.defineFailed("Invalid database") |
|---|
| 170 |
|
|---|
| 171 |
def dictCode_550_match(self, line): |
|---|
| 172 |
"""Invalid database""" |
|---|
| 173 |
self.mode = "ready" |
|---|
| 174 |
self.matchFailed("Invalid database") |
|---|
| 175 |
|
|---|
| 176 |
def dictCode_551_match(self, line): |
|---|
| 177 |
"""Invalid strategy""" |
|---|
| 178 |
self.mode = "ready" |
|---|
| 179 |
self.matchFailed("Invalid strategy") |
|---|
| 180 |
|
|---|
| 181 |
def dictCode_552_define(self, line): |
|---|
| 182 |
"""No match""" |
|---|
| 183 |
self.mode = "ready" |
|---|
| 184 |
self.defineFailed("No match") |
|---|
| 185 |
|
|---|
| 186 |
def dictCode_552_match(self, line): |
|---|
| 187 |
"""No match""" |
|---|
| 188 |
self.mode = "ready" |
|---|
| 189 |
self.matchFailed("No match") |
|---|
| 190 |
|
|---|
| 191 |
def dictCode_150_define(self, line): |
|---|
| 192 |
"""n definitions retrieved""" |
|---|
| 193 |
self.result = [] |
|---|
| 194 |
|
|---|
| 195 |
def dictCode_151_define(self, line): |
|---|
| 196 |
"""Definition text follows""" |
|---|
| 197 |
self.mode = "text" |
|---|
| 198 |
(word, line) = parseParam(line) |
|---|
| 199 |
(db, line) = parseParam(line) |
|---|
| 200 |
(dbdesc, line) = parseParam(line) |
|---|
| 201 |
if not (word and db and dbdesc): |
|---|
| 202 |
self.protocolError("Invalid server response") |
|---|
| 203 |
self.transport.loseConnection() |
|---|
| 204 |
else: |
|---|
| 205 |
self.result.append(Definition(word, db, dbdesc, [])) |
|---|
| 206 |
self.data = [] |
|---|
| 207 |
|
|---|
| 208 |
def dictCode_152_match(self, line): |
|---|
| 209 |
"""n matches found, text follows""" |
|---|
| 210 |
self.mode = "text" |
|---|
| 211 |
self.result = [] |
|---|
| 212 |
self.data = [] |
|---|
| 213 |
|
|---|
| 214 |
def dictCode_text_define(self, line): |
|---|
| 215 |
"""A line of definition text received""" |
|---|
| 216 |
res = parseText(line) |
|---|
| 217 |
if res == None: |
|---|
| 218 |
self.mode = "command" |
|---|
| 219 |
self.result[-1].text = self.data |
|---|
| 220 |
self.data = None |
|---|
| 221 |
else: |
|---|
| 222 |
self.data.append(line) |
|---|
| 223 |
|
|---|
| 224 |
def dictCode_text_match(self, line): |
|---|
| 225 |
"""One line of match text received""" |
|---|
| 226 |
def l(s): |
|---|
| 227 |
p1, t = parseParam(s) |
|---|
| 228 |
p2, t = parseParam(t) |
|---|
| 229 |
return (p1, p2) |
|---|
| 230 |
res = parseText(line) |
|---|
| 231 |
if res == None: |
|---|
| 232 |
self.mode = "command" |
|---|
| 233 |
self.result = map(l, self.data) |
|---|
| 234 |
self.data = None |
|---|
| 235 |
else: |
|---|
| 236 |
self.data.append(line) |
|---|
| 237 |
|
|---|
| 238 |
def dictCode_250_define(self, line): |
|---|
| 239 |
"""ok""" |
|---|
| 240 |
t = self.result |
|---|
| 241 |
self.result = None |
|---|
| 242 |
self.state = "ready" |
|---|
| 243 |
self.defineDone(t) |
|---|
| 244 |
|
|---|
| 245 |
def dictCode_250_match(self, line): |
|---|
| 246 |
"""ok""" |
|---|
| 247 |
t = self.result |
|---|
| 248 |
self.result = None |
|---|
| 249 |
self.state = "ready" |
|---|
| 250 |
self.matchDone(t) |
|---|
| 251 |
|
|---|
| 252 |
def protocolError(self, reason): |
|---|
| 253 |
"""override to catch unexpected dict protocol conditions""" |
|---|
| 254 |
pass |
|---|
| 255 |
|
|---|
| 256 |
def dictConnected(self): |
|---|
| 257 |
"""override to be notified when the server is ready to accept commands""" |
|---|
| 258 |
pass |
|---|
| 259 |
|
|---|
| 260 |
def defineFailed(self, reason): |
|---|
| 261 |
"""override to catch reasonable failure responses to DEFINE""" |
|---|
| 262 |
pass |
|---|
| 263 |
|
|---|
| 264 |
def defineDone(self, result): |
|---|
| 265 |
"""override to catch succesful DEFINE""" |
|---|
| 266 |
pass |
|---|
| 267 |
|
|---|
| 268 |
def matchFailed(self, reason): |
|---|
| 269 |
"""override to catch resonable failure responses to MATCH""" |
|---|
| 270 |
pass |
|---|
| 271 |
|
|---|
| 272 |
def matchDone(self, result): |
|---|
| 273 |
"""override to catch succesful MATCH""" |
|---|
| 274 |
pass |
|---|
| 275 |
|
|---|
| 276 |
|
|---|
| 277 |
class InvalidResponse(Exception): |
|---|
| 278 |
pass |
|---|
| 279 |
|
|---|
| 280 |
|
|---|
| 281 |
class DictLookup(DictClient): |
|---|
| 282 |
"""Utility class for a single dict transaction. To be used with DictLookupFactory""" |
|---|
| 283 |
|
|---|
| 284 |
def protocolError(self, reason): |
|---|
| 285 |
if not self.factory.done: |
|---|
| 286 |
self.factory.d.errback(InvalidResponse(reason)) |
|---|
| 287 |
self.factory.clientDone() |
|---|
| 288 |
|
|---|
| 289 |
def dictConnected(self): |
|---|
| 290 |
if self.factory.queryType == "define": |
|---|
| 291 |
apply(self.sendDefine, self.factory.param) |
|---|
| 292 |
elif self.factory.queryType == "match": |
|---|
| 293 |
apply(self.sendMatch, self.factory.param) |
|---|
| 294 |
|
|---|
| 295 |
def defineFailed(self, reason): |
|---|
| 296 |
self.factory.d.callback([]) |
|---|
| 297 |
self.factory.clientDone() |
|---|
| 298 |
self.transport.loseConnection() |
|---|
| 299 |
|
|---|
| 300 |
def defineDone(self, result): |
|---|
| 301 |
self.factory.d.callback(result) |
|---|
| 302 |
self.factory.clientDone() |
|---|
| 303 |
self.transport.loseConnection() |
|---|
| 304 |
|
|---|
| 305 |
def matchFailed(self, reason): |
|---|
| 306 |
self.factory.d.callback([]) |
|---|
| 307 |
self.factory.clientDone() |
|---|
| 308 |
self.transport.loseConnection() |
|---|
| 309 |
|
|---|
| 310 |
def matchDone(self, result): |
|---|
| 311 |
self.factory.d.callback(result) |
|---|
| 312 |
self.factory.clientDone() |
|---|
| 313 |
self.transport.loseConnection() |
|---|
| 314 |
|
|---|
| 315 |
|
|---|
| 316 |
class DictLookupFactory(protocol.ClientFactory): |
|---|
| 317 |
"""Utility factory for a single dict transaction""" |
|---|
| 318 |
protocol = DictLookup |
|---|
| 319 |
done = None |
|---|
| 320 |
|
|---|
| 321 |
def __init__(self, queryType, param, d): |
|---|
| 322 |
self.queryType = queryType |
|---|
| 323 |
self.param = param |
|---|
| 324 |
self.d = d |
|---|
| 325 |
self.done = 0 |
|---|
| 326 |
|
|---|
| 327 |
def clientDone(self): |
|---|
| 328 |
"""Called by client when done.""" |
|---|
| 329 |
self.done = 1 |
|---|
| 330 |
del self.d |
|---|
| 331 |
|
|---|
| 332 |
def clientConnectionFailed(self, connector, error): |
|---|
| 333 |
self.d.errback(error) |
|---|
| 334 |
|
|---|
| 335 |
def clientConnectionLost(self, connector, error): |
|---|
| 336 |
if not self.done: |
|---|
| 337 |
self.d.errback(error) |
|---|
| 338 |
|
|---|
| 339 |
def buildProtocol(self, addr): |
|---|
| 340 |
p = self.protocol() |
|---|
| 341 |
p.factory = self |
|---|
| 342 |
return p |
|---|
| 343 |
|
|---|
| 344 |
|
|---|
| 345 |
def define(host, port, database, word): |
|---|
| 346 |
"""Look up a word using a dict server""" |
|---|
| 347 |
d = defer.Deferred() |
|---|
| 348 |
factory = DictLookupFactory("define", (database, word), d) |
|---|
| 349 |
|
|---|
| 350 |
from twisted.internet import reactor |
|---|
| 351 |
reactor.connectTCP(host, port, factory) |
|---|
| 352 |
return d |
|---|
| 353 |
|
|---|
| 354 |
def match(host, port, database, strategy, word): |
|---|
| 355 |
"""Match a word using a dict server""" |
|---|
| 356 |
d = defer.Deferred() |
|---|
| 357 |
factory = DictLookupFactory("match", (database, strategy, word), d) |
|---|
| 358 |
|
|---|
| 359 |
from twisted.internet import reactor |
|---|
| 360 |
reactor.connectTCP(host, port, factory) |
|---|
| 361 |
return d |
|---|
| 362 |
|
|---|