this is based on calsyslab project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

466 lines
15 KiB

1 year ago
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. """Death by Captcha HTTP and socket API clients.
  4. There are two types of Death by Captcha (DBC hereinafter) API: HTTP and
  5. socket ones. Both offer the same functionalily, with the socket API
  6. sporting faster responses and using way less connections.
  7. To access the socket API, use SocketClient class; for the HTTP API, use
  8. HttpClient class. Both are thread-safe. SocketClient keeps a persistent
  9. connection opened and serializes all API requests sent through it, thus
  10. it is advised to keep a pool of them if you're script is heavily
  11. multithreaded.
  12. Both SocketClient and HttpClient give you the following methods:
  13. get_user()
  14. Returns your DBC account details as a dict with the following keys:
  15. "user": your account numeric ID; if login fails, it will be the only
  16. item with the value of 0;
  17. "rate": your CAPTCHA rate, i.e. how much you will be charged for one
  18. solved CAPTCHA in US cents;
  19. "balance": your DBC account balance in US cents;
  20. "is_banned": flag indicating whether your account is suspended or not.
  21. get_balance()
  22. Returns your DBC account balance in US cents.
  23. get_captcha(cid)
  24. Returns an uploaded CAPTCHA details as a dict with the following keys:
  25. "captcha": the CAPTCHA numeric ID; if no such CAPTCHAs found, it will
  26. be the only item with the value of 0;
  27. "text": the CAPTCHA text, if solved, otherwise None;
  28. "is_correct": flag indicating whether the CAPTCHA was solved correctly
  29. (DBC can detect that in rare cases).
  30. The only argument `cid` is the CAPTCHA numeric ID.
  31. get_text(cid)
  32. Returns an uploaded CAPTCHA text (None if not solved). The only argument
  33. `cid` is the CAPTCHA numeric ID.
  34. report(cid)
  35. Reports an incorrectly solved CAPTCHA. The only argument `cid` is the
  36. CAPTCHA numeric ID. Returns True on success, False otherwise.
  37. upload(captcha)
  38. Rploads a CAPTCHA. The only argument `captcha` can be either file-like
  39. object (any object with `read` method defined, actually, so StringIO
  40. will do), or CAPTCHA image file name. On successul upload you'll get
  41. the CAPTCHA details dict (see get_captcha() method).
  42. NOTE: AT THIS POINT THE UPLOADED CAPTCHA IS NOT SOLVED YET! You have
  43. to poll for its status periodically using get_captcha() or get_text()
  44. method until the CAPTCHA is solved and you get the text.
  45. decode(captcha, timeout=DEFAULT_TIMEOUT)
  46. A convenient method that uploads a CAPTCHA and polls for its status
  47. periodically, but no longer than `timeout` (defaults to 60 seconds).
  48. If solved, you'll get the CAPTCHA details dict (see get_captcha()
  49. method for details). See upload() method for details on `captcha`
  50. argument.
  51. Visit http://www.deathbycaptcha.com/user/api for updates.
  52. """
  53. import base64
  54. import binascii
  55. import errno
  56. import imghdr
  57. import random
  58. import os
  59. import select
  60. import socket
  61. import sys
  62. import threading
  63. import time
  64. import urllib
  65. import urllib2
  66. try:
  67. from json import read as json_decode, write as json_encode
  68. except ImportError:
  69. try:
  70. from json import loads as json_decode, dumps as json_encode
  71. except ImportError:
  72. from simplejson import loads as json_decode, dumps as json_encode
  73. # API version and unique software ID
  74. API_VERSION = 'DBC/Python v4.1.2'
  75. # Default CAPTCHA timeout and decode() polling interval
  76. DEFAULT_TIMEOUT = 60
  77. POLLS_INTERVAL = 5
  78. # Base HTTP API url
  79. HTTP_BASE_URL = 'http://api.dbcapi.me/api'
  80. # Preferred HTTP API server's response content type, do not change
  81. HTTP_RESPONSE_TYPE = 'application/json'
  82. # Socket API server's host & ports range
  83. SOCKET_HOST = 'api.dbcapi.me'
  84. SOCKET_PORTS = range(8123, 8131)
  85. def _load_image(captcha):
  86. if hasattr(captcha, 'read'):
  87. img = captcha.read()
  88. elif type(captcha) == bytearray:
  89. img = captcha
  90. else:
  91. img = ''
  92. try:
  93. captcha_file = open(captcha, 'rb')
  94. except Exception:
  95. raise
  96. else:
  97. img = captcha_file.read()
  98. captcha_file.close()
  99. if not len(img):
  100. raise ValueError('CAPTCHA image is empty')
  101. elif imghdr.what(None, img) is None:
  102. raise TypeError('Unknown CAPTCHA image type')
  103. else:
  104. return img
  105. class AccessDeniedException(Exception):
  106. pass
  107. class Client(object):
  108. """Death by Captcha API Client."""
  109. def __init__(self, username, password):
  110. self.is_verbose = False
  111. self.userpwd = {'username': username, 'password': password}
  112. def _log(self, cmd, msg=''):
  113. if self.is_verbose:
  114. print '%d %s %s' % (time.time(), cmd, msg.rstrip())
  115. return self
  116. def close(self):
  117. pass
  118. def connect(self):
  119. pass
  120. def get_user(self):
  121. """Fetch user details -- ID, balance, rate and banned status."""
  122. raise NotImplementedError()
  123. def get_balance(self):
  124. """Fetch user balance (in US cents)."""
  125. return self.get_user().get('balance')
  126. def get_captcha(self, cid):
  127. """Fetch a CAPTCHA details -- ID, text and correctness flag."""
  128. raise NotImplementedError()
  129. def get_text(self, cid):
  130. """Fetch a CAPTCHA text."""
  131. return self.get_captcha(cid).get('text') or None
  132. def report(self, cid):
  133. """Report a CAPTCHA as incorrectly solved."""
  134. raise NotImplementedError()
  135. def upload(self, captcha):
  136. """Upload a CAPTCHA.
  137. Accepts file names and file-like objects. Returns CAPTCHA details
  138. dict on success.
  139. """
  140. raise NotImplementedError()
  141. def decode(self, captcha, timeout=DEFAULT_TIMEOUT):
  142. """Try to solve a CAPTCHA.
  143. See Client.upload() for arguments details.
  144. Uploads a CAPTCHA, polls for its status periodically with arbitrary
  145. timeout (in seconds), returns CAPTCHA details if (correctly) solved.
  146. """
  147. deadline = time.time() + (max(0, timeout) or DEFAULT_TIMEOUT)
  148. uploaded_captcha = self.upload(captcha)
  149. if uploaded_captcha:
  150. while deadline > time.time() and not uploaded_captcha.get('text'):
  151. time.sleep(POLLS_INTERVAL)
  152. pulled = self.get_captcha(uploaded_captcha['captcha'])
  153. if pulled['captcha']==uploaded_captcha['captcha']:
  154. uploaded_captcha = pulled
  155. if uploaded_captcha.get('text') and uploaded_captcha.get('is_correct'):
  156. return uploaded_captcha
  157. class HttpClient(Client):
  158. """Death by Captcha HTTP API client."""
  159. def __init__(self, *args):
  160. Client.__init__(self, *args)
  161. self.opener = urllib2.build_opener(urllib2.HTTPRedirectHandler())
  162. def _call(self, cmd, payload=None, headers=None):
  163. if headers is None:
  164. headers = {}
  165. headers['Accept'] = HTTP_RESPONSE_TYPE
  166. headers['User-Agent'] = API_VERSION
  167. if hasattr(payload, 'items'):
  168. payload = urllib.urlencode(payload)
  169. self._log('SEND', '%s %d %s' % (cmd, len(payload), payload))
  170. else:
  171. self._log('SEND', '%s' % cmd)
  172. if payload is not None:
  173. headers['Content-Length'] = len(payload)
  174. try:
  175. response = self.opener.open(urllib2.Request(
  176. HTTP_BASE_URL + '/' + cmd.strip('/'),
  177. data=payload,
  178. headers=headers
  179. )).read()
  180. except urllib2.HTTPError, err:
  181. if 403 == err.code:
  182. raise AccessDeniedException('Access denied, please check your credentials and/or balance')
  183. elif 400 == err.code or 413 == err.code:
  184. raise ValueError("CAPTCHA was rejected by the service, check if it's a valid image")
  185. elif 503 == err.code:
  186. raise OverflowError("CAPTCHA was rejected due to service overload, try again later")
  187. else:
  188. raise err
  189. else:
  190. self._log('RECV', '%d %s' % (len(response), response))
  191. try:
  192. return json_decode(response)
  193. except Exception:
  194. raise RuntimeError('Invalid API response')
  195. return {}
  196. def get_user(self):
  197. return self._call('user', self.userpwd.copy()) or {'user': 0}
  198. def get_captcha(self, cid):
  199. return self._call('captcha/%d' % cid) or {'captcha': 0}
  200. def report(self, cid):
  201. return not self._call('captcha/%d/report' % cid,
  202. self.userpwd.copy()).get('is_correct')
  203. def upload(self, captcha):
  204. boundary = binascii.hexlify(os.urandom(16))
  205. body = '\r\n'.join(('\r\n'.join((
  206. '--%s' % boundary,
  207. 'Content-Disposition: form-data; name="%s"' % k,
  208. 'Content-Type: text/plain',
  209. 'Content-Length: %d' % len(str(v)),
  210. '',
  211. str(v)
  212. ))) for k, v in self.userpwd.items())
  213. img = _load_image(captcha)
  214. body += '\r\n'.join((
  215. '',
  216. '--%s' % boundary,
  217. 'Content-Disposition: form-data; name="captchafile"; filename="captcha"',
  218. 'Content-Type: application/octet-stream',
  219. 'Content-Length: %d' % len(img),
  220. '',
  221. img,
  222. '--%s--' % boundary,
  223. ''
  224. ))
  225. response = self._call('captcha', body, {
  226. 'Content-Type': 'multipart/form-data; boundary="%s"' % boundary
  227. }) or {}
  228. if response.get('captcha'):
  229. return response
  230. class SocketClient(Client):
  231. """Death by Captcha socket API client."""
  232. TERMINATOR = '\r\n'
  233. def __init__(self, *args):
  234. Client.__init__(self, *args)
  235. self.socket_lock = threading.Lock()
  236. self.socket = None
  237. def close(self):
  238. if self.socket:
  239. self._log('CLOSE')
  240. try:
  241. self.socket.shutdown(socket.SHUT_RDWR)
  242. except socket.error:
  243. pass
  244. finally:
  245. self.socket.close()
  246. self.socket = None
  247. def connect(self):
  248. if not self.socket:
  249. self._log('CONN')
  250. host = (socket.gethostbyname(SOCKET_HOST),
  251. random.choice(SOCKET_PORTS))
  252. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  253. self.socket.settimeout(0)
  254. try:
  255. self.socket.connect(host)
  256. except socket.error, err:
  257. if err.args[0] not in (errno.EAGAIN, errno.EWOULDBLOCK, errno.EINPROGRESS):
  258. self.close()
  259. raise err
  260. return self.socket
  261. def __del__(self):
  262. self.close()
  263. def _sendrecv(self, sock, buf):
  264. self._log('SEND', buf)
  265. fds = [sock]
  266. buf += self.TERMINATOR
  267. response = ''
  268. while True:
  269. rds, wrs, exs = select.select((not buf and fds) or [],
  270. (buf and fds) or [],
  271. fds,
  272. POLLS_INTERVAL)
  273. if exs:
  274. raise IOError('select() failed')
  275. try:
  276. if wrs:
  277. while buf:
  278. buf = buf[wrs[0].send(buf):]
  279. elif rds:
  280. while True:
  281. s = rds[0].recv(256)
  282. if not s:
  283. raise IOError('recv(): connection lost')
  284. else:
  285. response += s
  286. except socket.error, err:
  287. if err.args[0] not in (errno.EAGAIN, errno.EWOULDBLOCK, errno.EINPROGRESS):
  288. raise err
  289. if response.endswith(self.TERMINATOR):
  290. self._log('RECV', response)
  291. return response.rstrip(self.TERMINATOR)
  292. raise IOError('send/recv timed out')
  293. def _call(self, cmd, data=None):
  294. if data is None:
  295. data = {}
  296. data['cmd'] = cmd
  297. data['version'] = API_VERSION
  298. request = json_encode(data)
  299. response = None
  300. for _ in range(2):
  301. if not self.socket and cmd != 'login':
  302. self._call('login', self.userpwd.copy())
  303. self.socket_lock.acquire()
  304. try:
  305. sock = self.connect()
  306. response = self._sendrecv(sock, request)
  307. except IOError, err:
  308. sys.stderr.write(str(err) + "\n")
  309. self.close()
  310. except socket.error, err:
  311. sys.stderr.write(str(err) + "\n")
  312. self.close()
  313. raise IOError('Connection refused')
  314. else:
  315. break
  316. finally:
  317. self.socket_lock.release()
  318. if response is None:
  319. raise IOError('Connection lost or timed out during API request')
  320. try:
  321. response = json_decode(response)
  322. except Exception:
  323. raise RuntimeError('Invalid API response')
  324. if not response.get('error'):
  325. return response
  326. error = response['error']
  327. if error in ('not-logged-in', 'invalid-credentials'):
  328. raise AccessDeniedException('Access denied, check your credentials')
  329. elif 'banned' == error:
  330. raise AccessDeniedException('Access denied, account is suspended')
  331. elif 'insufficient-funds' == error:
  332. raise AccessDeniedException('CAPTCHA was rejected due to low balance')
  333. elif 'invalid-captcha' == error:
  334. raise ValueError('CAPTCHA is not a valid image')
  335. elif 'service-overload' == error:
  336. raise OverflowError('CAPTCHA was rejected due to service overload, try again later')
  337. else:
  338. self.socket_lock.acquire()
  339. self.close()
  340. self.socket_lock.release()
  341. raise RuntimeError('API server error occured: %s' % error)
  342. def get_user(self):
  343. return self._call('user') or {'user': 0}
  344. def get_captcha(self, cid):
  345. return self._call('captcha', {'captcha': cid}) or {'captcha': 0}
  346. def upload(self, captcha):
  347. response = self._call('upload', {
  348. 'captcha': base64.b64encode(_load_image(captcha))
  349. })
  350. if response.get('captcha'):
  351. uploaded_captcha = dict(
  352. (k, response.get(k))
  353. for k in ('captcha', 'text', 'is_correct')
  354. )
  355. if not uploaded_captcha['text']:
  356. uploaded_captcha['text'] = None
  357. return uploaded_captcha
  358. def report(self, cid):
  359. return not self._call('report', {'captcha': cid}).get('is_correct')
  360. if '__main__' == __name__:
  361. # Put your DBC username & password here:
  362. #client = HttpClient(sys.argv[1], sys.argv[2])
  363. client = SocketClient(sys.argv[1], sys.argv[2])
  364. client.is_verbose = True
  365. print 'Your balance is %s US cents' % client.get_balance()
  366. for fn in sys.argv[3:]:
  367. try:
  368. # Put your CAPTCHA image file name or file-like object, and optional
  369. # solving timeout (in seconds) here:
  370. captcha = client.decode(fn, DEFAULT_TIMEOUT)
  371. except Exception, e:
  372. sys.stderr.write('Failed uploading CAPTCHA: %s\n' % (e, ))
  373. captcha = None
  374. if captcha:
  375. print 'CAPTCHA %d solved: %s' % \
  376. (captcha['captcha'], captcha['text'])
  377. # Report as incorrectly solved if needed. Make sure the CAPTCHA was
  378. # in fact incorrectly solved!
  379. #try:
  380. # client.report(captcha['captcha'])
  381. #except Exception, e:
  382. # sys.stderr.write('Failed reporting CAPTCHA: %s\n' % (e, ))