Skip to content

Commit 04e6b35

Browse files
frankli0324Pairman
andauthored
improve captcha solving mechanisms (#19)
* add solver callback mechanism * add PIL viewer for displaying captcha in CLI * remove rsbbs captcha viewer * bump version and fix dependencies --------- Co-authored-by: Pairman Guo <[email protected]>
1 parent f807256 commit 04e6b35

File tree

6 files changed

+82
-80
lines changed

6 files changed

+82
-80
lines changed

libxduauth/sites/ids.py

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import time
2-
from io import BytesIO
32

4-
from PIL import Image
53
from bs4 import BeautifulSoup
64

75
from ..AuthSession import AuthSession
86
from ..utils.page import parse_form_hidden_inputs
97
from ..utils.aes import encrypt
8+
from ..utils.vcode import get_solver
109

1110

1211
class IDSSession(AuthSession):
1312
cookie_name = 'ids'
1413

15-
def __init__(
16-
self, target, username, password,
17-
*args, **kwargs
18-
):
14+
def __init__(self, target, username, password):
1915
super().__init__(f'{self.cookie_name}_{username}')
2016
if self.is_logged_in():
2117
return
@@ -25,34 +21,23 @@ def __init__(
2521
'http://ids.xidian.edu.cn/authserver/login',
2622
params={'service': target}
2723
).text
28-
is_need_captcha = self.get(
24+
while self.get(
2925
'https://ids.xidian.edu.cn/authserver/checkNeedCaptcha.htl',
3026
params={'username': username, '_': str(int(time.time() * 1000))}
31-
).json()['isNeed']
32-
if is_need_captcha:
33-
captcha = self.get(
34-
'https://ids.xidian.edu.cn/authserver/common/openSliderCaptcha.htl',
35-
params={'_': str(int(time.time() * 1000))}
36-
)
37-
# 返回: {
38-
# 'bigImage': ..., # 背景图(base64)
39-
# 'smallImage': ..., # 滑块图(base64)
40-
# 'tagWidth": 93, # 无用, 恒93
41-
# 'yHeight': 0 # 无用, 恒0
42-
# }
43-
img = Image.open(BytesIO(captcha.json()['bigImage']))
44-
img.show()
45-
# move_len: 背景图左侧到滑块目标位置左侧的宽度
46-
move_len = input('滑块位移:')
47-
# canvasLength: canvas宽度, 硬编码280
48-
# moveLength: 按比例缩放后的滑块位移, 有容错
49-
verify = self.post(
27+
).json()['isNeed']:
28+
if self.post(
5029
'https://ids.xidian.edu.cn/authserver/common/verifySliderCaptcha.htl',
5130
data={
52-
'canvasLength': '280',
53-
'moveLength': str(move_len * 280 // img.width)
31+
# canvasLength: canvas宽度, 硬编码280
32+
'canvasLength': '280',
33+
# moveLength: 按比例缩放后的滑块位移, 有容错
34+
'moveLength': str(get_solver('ids.xidian.edu.cn')(self.get(
35+
'https://ids.xidian.edu.cn/authserver/common/openSliderCaptcha.htl',
36+
params={'_': str(int(time.time() * 1000))}
37+
).json()))
5438
}
55-
)
39+
).json()['errorMsg'] == 'success':
40+
break
5641
# 返回: {
5742
# 'errorCode': ..., # 验证通过时为1
5843
# 'errorMsg': ... # 验证通过时为'success'

libxduauth/sites/rsbbs.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ..AuthSession import AuthSession
77
from ..utils.page import parse_form_hidden_inputs
8-
from ..utils.vcode import _process_vcode
8+
from ..utils.vcode import get_solver
99

1010

1111
class RSBBSSession(AuthSession):
@@ -26,13 +26,9 @@ def login(self, username, password):
2626
soup = BeautifulSoup(login.text, 'lxml')
2727

2828
img = soup.find('img', {'class': 'seccodeimg'}).get('src')
29-
img = _process_vcode(Image.open(
30-
BytesIO(self.get(f'http://{self.HOST}/{img}', headers={
31-
'Referer': login.url
32-
}).content)
33-
))
34-
img.show()
35-
vcode = input('验证码:')
29+
vcode = get_solver('rsbbs.xidian.edu.cn')(self.get(f'http://{self.HOST}/{img}', headers={
30+
'Referer': login.url
31+
}).content)
3632
page = self.post(
3733
f'http://{self.HOST}/' +
3834
soup.find('form', id='loginform').get('action'), data=dict(

libxduauth/sites/xk.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import json
22
from base64 import b64encode, b64decode
3-
from io import BytesIO
43
from re import search
54

65
import requests
76
from Crypto.Cipher import AES
87
from Crypto.Util.Padding import pad
9-
from PIL import Image
108

9+
from ..utils.vcode import get_solver
1110
from ..AuthSession import AuthSession
1211

1312

@@ -70,10 +69,7 @@ def login(self, username, password):
7069

7170
# show captcha
7271
captcha_img = captcha['captcha'][22:] # data:image/png;base64,
73-
Image.open(BytesIO(b64decode(captcha_img))).show()
74-
75-
# TODO:input function shouldn't exist here
76-
captcha_code = input('验证码:')
72+
captcha_code = get_solver('xk.xidian.edu.cn')(b64decode(captcha_img))
7773

7874
login_resp = self.post(f'{self.BASE}/auth/login', data={
7975
'loginname': username,

libxduauth/utils/cli_viewer.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from PIL.ImageShow import Viewer, register as register_pil_imshow
2+
from PIL import Image
3+
4+
5+
def _image_to_ascii(img, size=(80, 16), invert_pallete=True):
6+
# convert image to ascii art for cli view
7+
# given char dimension 24*16, optimal sizes are
8+
# (80, 16) for xk, (80, 21) for rsbbs
9+
if invert_pallete:
10+
chs = '@&%QWNM0gB$#DR8mHXKAUbGOpV4d9h6Pkqwaxoenut1ivsz/*cr!+<>;=^:\'-.` '
11+
else:
12+
chs = ' `.-\':^=;><+!rc*/zsvi1tuneoxawqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@'
13+
w, h = size
14+
img.load()
15+
im = img.im.convert('L').resize((w, h))
16+
return '\n'.join(''.join(
17+
chs[im[i] // 4] for i in range(y, y + w)
18+
) for y in range(0, w * h, w))
19+
20+
21+
class _CliViewer(Viewer):
22+
def show(self, image: Image.Image, **options: Image.Any) -> int:
23+
print(_image_to_ascii(image))
24+
# always attempt other viewers
25+
return False
26+
27+
def register():
28+
register_pil_imshow(_CliViewer, 0)

libxduauth/utils/vcode.py

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
1-
class Processor:
2-
def __init__(self, img):
3-
self.img = img.convert('L')
4-
self.img_arr = self.img.load()
5-
self.paint()
6-
7-
DX = [1, 0, -1, 0]
8-
DY = [0, 1, 0, -1]
9-
10-
def paint(self):
11-
w, h = self.img.size
12-
visited = set()
13-
q = []
14-
q.append((0, 0, 255))
15-
while q:
16-
x, y, value = q.pop()
17-
if x < 0 or y < 0 or x >= w or y >= h or \
18-
(x, y) in visited:
19-
continue
20-
visited.add((x, y))
21-
for i in range(4):
22-
try:
23-
pixel = self.img_arr[x + self.DX[i], y + self.DY[i]]
24-
except IndexError:
25-
continue
26-
if abs(pixel - self.img_arr[x, y]) > 5:
27-
q.append((x + self.DX[i], y + self.DY[i], 255 - value))
28-
else:
29-
q.append((x + self.DX[i], y + self.DY[i], value))
30-
self.img_arr[x, y] = value
31-
32-
33-
def _process_vcode(img):
34-
p = Processor(img)
35-
return p.img
1+
from base64 import b64decode
2+
from typing import Callable
3+
from io import BytesIO
4+
from PIL import Image
5+
6+
7+
def _default_solver(data):
8+
Image.open(BytesIO(data)).show()
9+
return input('验证码: ')
10+
11+
12+
def _ids_solver(data):
13+
# data is {
14+
# 'bigImage': ..., # 背景图(base64)
15+
# 'smallImage': ..., # 滑块图(base64)
16+
# 'tagWidth": 93, # 无用, 恒93
17+
# 'yHeight': 0 # 无用, 恒0
18+
# }
19+
img = Image.open(BytesIO(b64decode(data['bigImage'])))
20+
img.show()
21+
22+
# 输入背景图左侧到滑块目标位置左侧的宽度
23+
return int(input('滑块位移: ')) * 280 // img.width
24+
25+
26+
_solvers = {
27+
'ids.xidian.edu.cn': _ids_solver,
28+
}
29+
30+
31+
def get_solver(key) -> Callable:
32+
return _solvers.get(key, _default_solver)

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="libxduauth",
8-
version="1.8.2",
8+
version="1.9.0",
99
author="Frank",
1010
author_email="[email protected]",
1111
description="login utilities for XDU",
@@ -19,7 +19,7 @@
1919
],
2020
python_requires='>=3.7',
2121
install_requires=[
22-
"requests", "bs4", "pycryptodome",
22+
"requests", "bs4", "pycryptodome", "lxml",
2323
"importlib-resources", "Pillow",
2424
],
2525
)

0 commit comments

Comments
 (0)