Skip to content

Commit

Permalink
improve captcha solving mechanisms (#19)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
frankli0324 and Pairman authored Jun 25, 2024
1 parent f807256 commit 04e6b35
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 80 deletions.
43 changes: 14 additions & 29 deletions libxduauth/sites/ids.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import time
from io import BytesIO

from PIL import Image
from bs4 import BeautifulSoup

from ..AuthSession import AuthSession
from ..utils.page import parse_form_hidden_inputs
from ..utils.aes import encrypt
from ..utils.vcode import get_solver


class IDSSession(AuthSession):
cookie_name = 'ids'

def __init__(
self, target, username, password,
*args, **kwargs
):
def __init__(self, target, username, password):
super().__init__(f'{self.cookie_name}_{username}')
if self.is_logged_in():
return
Expand All @@ -25,34 +21,23 @@ def __init__(
'http://ids.xidian.edu.cn/authserver/login',
params={'service': target}
).text
is_need_captcha = self.get(
while self.get(
'https://ids.xidian.edu.cn/authserver/checkNeedCaptcha.htl',
params={'username': username, '_': str(int(time.time() * 1000))}
).json()['isNeed']
if is_need_captcha:
captcha = self.get(
'https://ids.xidian.edu.cn/authserver/common/openSliderCaptcha.htl',
params={'_': str(int(time.time() * 1000))}
)
# 返回: {
# 'bigImage': ..., # 背景图(base64)
# 'smallImage': ..., # 滑块图(base64)
# 'tagWidth": 93, # 无用, 恒93
# 'yHeight': 0 # 无用, 恒0
# }
img = Image.open(BytesIO(captcha.json()['bigImage']))
img.show()
# move_len: 背景图左侧到滑块目标位置左侧的宽度
move_len = input('滑块位移:')
# canvasLength: canvas宽度, 硬编码280
# moveLength: 按比例缩放后的滑块位移, 有容错
verify = self.post(
).json()['isNeed']:
if self.post(
'https://ids.xidian.edu.cn/authserver/common/verifySliderCaptcha.htl',
data={
'canvasLength': '280',
'moveLength': str(move_len * 280 // img.width)
# canvasLength: canvas宽度, 硬编码280
'canvasLength': '280',
# moveLength: 按比例缩放后的滑块位移, 有容错
'moveLength': str(get_solver('ids.xidian.edu.cn')(self.get(
'https://ids.xidian.edu.cn/authserver/common/openSliderCaptcha.htl',
params={'_': str(int(time.time() * 1000))}
).json()))
}
)
).json()['errorMsg'] == 'success':
break
# 返回: {
# 'errorCode': ..., # 验证通过时为1
# 'errorMsg': ... # 验证通过时为'success'
Expand Down
12 changes: 4 additions & 8 deletions libxduauth/sites/rsbbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from ..AuthSession import AuthSession
from ..utils.page import parse_form_hidden_inputs
from ..utils.vcode import _process_vcode
from ..utils.vcode import get_solver


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

img = soup.find('img', {'class': 'seccodeimg'}).get('src')
img = _process_vcode(Image.open(
BytesIO(self.get(f'http://{self.HOST}/{img}', headers={
'Referer': login.url
}).content)
))
img.show()
vcode = input('验证码:')
vcode = get_solver('rsbbs.xidian.edu.cn')(self.get(f'http://{self.HOST}/{img}', headers={
'Referer': login.url
}).content)
page = self.post(
f'http://{self.HOST}/' +
soup.find('form', id='loginform').get('action'), data=dict(
Expand Down
8 changes: 2 additions & 6 deletions libxduauth/sites/xk.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import json
from base64 import b64encode, b64decode
from io import BytesIO
from re import search

import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from PIL import Image

from ..utils.vcode import get_solver
from ..AuthSession import AuthSession


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

# show captcha
captcha_img = captcha['captcha'][22:] # data:image/png;base64,
Image.open(BytesIO(b64decode(captcha_img))).show()

# TODO:input function shouldn't exist here
captcha_code = input('验证码:')
captcha_code = get_solver('xk.xidian.edu.cn')(b64decode(captcha_img))

login_resp = self.post(f'{self.BASE}/auth/login', data={
'loginname': username,
Expand Down
28 changes: 28 additions & 0 deletions libxduauth/utils/cli_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from PIL.ImageShow import Viewer, register as register_pil_imshow
from PIL import Image


def _image_to_ascii(img, size=(80, 16), invert_pallete=True):
# convert image to ascii art for cli view
# given char dimension 24*16, optimal sizes are
# (80, 16) for xk, (80, 21) for rsbbs
if invert_pallete:
chs = '@&%QWNM0gB$#DR8mHXKAUbGOpV4d9h6Pkqwaxoenut1ivsz/*cr!+<>;=^:\'-.` '
else:
chs = ' `.-\':^=;><+!rc*/zsvi1tuneoxawqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@'
w, h = size
img.load()
im = img.im.convert('L').resize((w, h))
return '\n'.join(''.join(
chs[im[i] // 4] for i in range(y, y + w)
) for y in range(0, w * h, w))


class _CliViewer(Viewer):
def show(self, image: Image.Image, **options: Image.Any) -> int:
print(_image_to_ascii(image))
# always attempt other viewers
return False

def register():
register_pil_imshow(_CliViewer, 0)
67 changes: 32 additions & 35 deletions libxduauth/utils/vcode.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
class Processor:
def __init__(self, img):
self.img = img.convert('L')
self.img_arr = self.img.load()
self.paint()

DX = [1, 0, -1, 0]
DY = [0, 1, 0, -1]

def paint(self):
w, h = self.img.size
visited = set()
q = []
q.append((0, 0, 255))
while q:
x, y, value = q.pop()
if x < 0 or y < 0 or x >= w or y >= h or \
(x, y) in visited:
continue
visited.add((x, y))
for i in range(4):
try:
pixel = self.img_arr[x + self.DX[i], y + self.DY[i]]
except IndexError:
continue
if abs(pixel - self.img_arr[x, y]) > 5:
q.append((x + self.DX[i], y + self.DY[i], 255 - value))
else:
q.append((x + self.DX[i], y + self.DY[i], value))
self.img_arr[x, y] = value


def _process_vcode(img):
p = Processor(img)
return p.img
from base64 import b64decode
from typing import Callable
from io import BytesIO
from PIL import Image


def _default_solver(data):
Image.open(BytesIO(data)).show()
return input('验证码: ')


def _ids_solver(data):
# data is {
# 'bigImage': ..., # 背景图(base64)
# 'smallImage': ..., # 滑块图(base64)
# 'tagWidth": 93, # 无用, 恒93
# 'yHeight': 0 # 无用, 恒0
# }
img = Image.open(BytesIO(b64decode(data['bigImage'])))
img.show()

# 输入背景图左侧到滑块目标位置左侧的宽度
return int(input('滑块位移: ')) * 280 // img.width


_solvers = {
'ids.xidian.edu.cn': _ids_solver,
}


def get_solver(key) -> Callable:
return _solvers.get(key, _default_solver)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="libxduauth",
version="1.8.2",
version="1.9.0",
author="Frank",
author_email="[email protected]",
description="login utilities for XDU",
Expand All @@ -19,7 +19,7 @@
],
python_requires='>=3.7',
install_requires=[
"requests", "bs4", "pycryptodome",
"requests", "bs4", "pycryptodome", "lxml",
"importlib-resources", "Pillow",
],
)

0 comments on commit 04e6b35

Please sign in to comment.