OLD | NEW |
| (Empty) |
1 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
2 # See LICENSE for details. | |
3 | |
4 | |
5 """ | |
6 Dict client protocol implementation. | |
7 | |
8 @author: U{Pavel Pergamenshchik<mailto:pp64@cornell.edu>} | |
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, reman
ingline)""" | |
18 if line == '': | |
19 return (None, '') | |
20 elif line[0] != '"': # atom | |
21 mode = 1 | |
22 else: # dqstring | |
23 mode = 2 | |
24 res = "" | |
25 io = StringIO(line) | |
26 if mode == 2: # skip the opening quote | |
27 io.read(1) | |
28 while 1: | |
29 a = io.read(1) | |
30 if a == '"': | |
31 if mode == 2: | |
32 io.read(1) # skip the separating space | |
33 return (res, io.read()) | |
34 elif a == '\\': | |
35 a = io.read(1) | |
36 if a == '': | |
37 return (None, line) # unexpected end of string | |
38 elif a == '': | |
39 if mode == 1: | |
40 return (res, io.read()) | |
41 else: | |
42 return (None, line) # unexpected end of string | |
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 # FIXME: proper quoting | |
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 # list of strings not terminated by newline | |
78 | |
79 class DictClient(basic.LineReceiver): | |
80 """dict (RFC2229) client""" | |
81 | |
82 data = None # multiline data | |
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: # garbage received, skip | |
107 return | |
108 if self.mode == "text": # we are receiving textual data | |
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.dictC
ode_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 # these two are just in case. In "ready" state, resu
lt and data | |
152 self.data = None # should be 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 r
eady state" | |
160 self.result = None | |
161 self.data = None | |
162 self.state = "match" | |
163 command = "MATCH %s %s %s" % (makeAtom(database), makeAtom(strategy), ma
keAtom(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 DictLookupFa
ctory""" | |
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 | |
OLD | NEW |