dict.py 11 KB

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