Skip to content

feat: zfw captcha #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flutter
Submodule .flutter updated 3281 files
Binary file added assets/captcha-solver-payment.tflite
Binary file not shown.
Binary file added assets/captcha-solver-zfw.tflite
Binary file not shown.
Binary file removed assets/captcha-solver.tflite
Binary file not shown.
Binary file added blobs/libtensorflowlite_c-linux.so
Binary file not shown.
Binary file added blobs/libtensorflowlite_c-macos.dylib
Binary file not shown.
Binary file added blobs/libtensorflowlite_c-win.dll
Binary file not shown.
140 changes: 123 additions & 17 deletions lib/page/public_widget/captcha_input_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,149 @@
// A captcha input dialog.

import 'dart:typed_data';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:watermeter/page/public_widget/toast.dart';
import 'package:image/image.dart' as img;
import 'package:tflite_flutter/tflite_flutter.dart';

enum DigitCaptchaType { payment, zfw }

class DigitCaptchaClientProvider {
static const String _interpreterAssetName = 'assets/captcha-solver.tflite';
// Ref: https://github.com/stalomeow/captcha-solver

static String _getInterpreterAssetName(DigitCaptchaType type) {
return 'assets/captcha-solver-${type.name.toLowerCase()}.tflite';
}

static double _lerp(double a, double b, double t) {
return a + (b - a) * t;
}

static num _sampleMin(img.Image image, List<int> bb, double u, double v) {
int x = _lerp(bb[0] * 1.0, bb[2] - 1.0, u).floor();
int y = _lerp(bb[1] * 1.0, bb[3] - 1.0, v).floor();
num px = min(image.getPixelClamped(x, y + 0).r,
image.getPixelClamped(x + 1, y + 0).r);
num py = min(image.getPixelClamped(x, y + 1).r,
image.getPixelClamped(x + 1, y + 1).r);
return min(px, py);
}

static List<int> _getbbox(img.Image image) {
int left = image.width;
int upper = image.height;
int right = 0; // Exclusive
int lower = 0; // Exclusive

static Future<String> infer(List<int> imageData) async {
// Ref: https://github.com/stalomeow/captcha-solver
for (int x = 0; x < image.width; x++) {
for (int y = 0; y < image.height; y++) {
num p = image.getPixel(x, y).r;

// Binarization
if (p < 0.98) {
continue;
}

left = min(left, x);
upper = min(upper, y);
right = max(right, x + 1);
lower = max(lower, y + 1);
}
}

// Expand the bounding box by 1 pixel
left = max(0, left - 1);
upper = max(0, upper - 1);
right = min(image.width, right + 1);
lower = min(image.height, lower + 1);

return [left, upper, right, lower];
}

static img.Image? _getImage(DigitCaptchaType type, List<int> imageData) {
img.Image image = img.decodeImage(Uint8List.fromList(imageData))!;
image = img.grayscale(image);
image = image.convert(
format: img.Format.float32, numChannels: 1); // 0-256 to 0-1

if (type == DigitCaptchaType.zfw) {
// Invert the image
for (int x = 0; x < image.width; x++) {
for (int y = 0; y < image.height; y++) {
image.setPixelR(x, y, 1.0 - image.getPixel(x, y).r);
}
}

List<int> bb = _getbbox(image);

// The numbers are too close
if (bb[2] - bb[0] < 44) {
return null;
}

// Align with the size of payment captcha
img.Image result = new img.Image(
width: 200, height: 80, format: img.Format.float32, numChannels: 1);
for (int x = 0; x < result.width; x++) {
for (int y = 0; y < result.height; y++) {
double u = x * 1.0 / result.width;
double v = y * 1.0 / result.height;
num r = _sampleMin(image, bb, u, v);
result.setPixelR(x, y, r);
}
}
image = result;
}

return image;
}

static int _argmax(List<double> list) {
int result = 0;
for (int i = 1; i < list.length; i++) {
if (list[i] > list[result]) {
result = i;
}
}
return result;
}

static int _getClassCount(DigitCaptchaType type) {
if (type == DigitCaptchaType.payment) {
return 9; // The payment captcha only contains number 1-9
}
return 10;
}

static int _getClassLabel(DigitCaptchaType type, int klass) {
if (type == DigitCaptchaType.payment) {
return klass + 1; // The payment captcha only contains number 1-9
}
return klass;
}

static Future<String?> infer(
DigitCaptchaType type, List<int> imageData) async {
img.Image? image = _getImage(type, imageData);

if (image == null) {
return null;
}

int dim2 = image.height;
int dim3 = image.width ~/ 4;
int classCount = _getClassCount(type);

var input = List.filled(dim2 * dim3, 0.0)
.reshape<double>([1, dim2, dim3, 1]) as List<List<List<List<double>>>>;
var output =
List.filled(9, 0.0).reshape<double>([1, 9]) as List<List<double>>;
var output = List.filled(classCount, 0.0).reshape<double>([1, classCount])
as List<List<double>>;

final interpreter = await Interpreter.fromAsset(_interpreterAssetName);
final interpreter =
await Interpreter.fromAsset(_getInterpreterAssetName(type));
List<int> nums = [];

// Four numbers
Expand All @@ -42,21 +158,11 @@ class DigitCaptchaClientProvider {
}

interpreter.run(input, output);
nums.add(_argmax(output[0]) + 1);
nums.add(_getClassLabel(type, _argmax(output[0])));
}

return nums.join('');
}

static int _argmax(List<double> list) {
int result = 0;
for (int i = 1; i < list.length; i++) {
if (list[i] > list[result]) {
result = i;
}
}
return result;
}
}

class CaptchaInputDialog extends StatelessWidget {
Expand Down
40 changes: 31 additions & 9 deletions lib/repository/schoolnet_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,21 +97,36 @@ class SchoolnetSession extends NetworkSession {
if (csrf.isEmpty || key.isEmpty) throw NotInitalizedException;

String lastErrorMessage = "";
for (int retry = 5; retry > 0; retry--) {
for (int retry = 10; retry > 0; retry--) {
// Refresh captcha
await _dio
.get('https://zfw.xidian.edu.cn/site/captcha', queryParameters: {
'refresh': 1,
'_': DateTime.now().millisecondsSinceEpoch,
});
// Get verifycode
String imgPath = parse(page.data.toString())
.getElementById("loginform-verifycode-image")
?.attributes["src"] ??
"";
var picture = await _dio
.get(imgPath, options: Options(responseType: ResponseType.bytes))
.get(
"https://zfw.xidian.edu.cn/site/captcha",
options: Options(responseType: ResponseType.bytes),
)
.then((data) => data.data);

String verifycode = retry == 1
String? verifycode = retry == 1
? captchaFunction != null
? await captchaFunction(picture)
: throw CaptchaFailedException() // The last try
: await DigitCaptchaClientProvider.infer(picture);
: await DigitCaptchaClientProvider.infer(
DigitCaptchaType.zfw, picture);

if (verifycode == null) {
log.info(
'[SchoolnetSession] Captcha is impossible to be inferred.');
retry++; // Do not count this try
continue;
}

log.info("[SchoolnetSession] verifycode is $verifycode");

// Encrypt the password
var rsaKey = RSAKeyParser().parse(key);
Expand Down Expand Up @@ -140,9 +155,16 @@ class SchoolnetSession extends NetworkSession {
if (!jsonDecode(page.data)["success"]) {
lastErrorMessage = jsonDecode(page.data)["message"] ?? "未知错误";
log.info(
"[SchoolNetSession] Attempt ${5 - retry} "
"[SchoolNetSession] Attempt ${11 - retry} "
"failed: $lastErrorMessage",
);

// No need to retry if the error is about username or password
if (lastErrorMessage.contains("用户名") ||
lastErrorMessage.contains("密码")) {
break;
}

continue;
}
// Login post
Expand Down
12 changes: 10 additions & 2 deletions lib/repository/xidian_ids/payment_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,19 @@ xh5zeF9usFgtdabgACU/cQIDAQAB
)
.then((data) => data.data);

String checkCode = retry == 1
String? checkCode = retry == 1
? captchaFunction != null
? await captchaFunction(picture)
: throw CaptchaFailedException() // The last try
: await DigitCaptchaClientProvider.infer(picture);
: await DigitCaptchaClientProvider.infer(
DigitCaptchaType.payment, picture);

if (checkCode == null) {
log.info(
'[PaymentSession][getOwe] Captcha is impossible to be inferred.');
retry++; // Do not count this try
continue;
}

log.info("[PaymentSession][getOwe] checkcode is $checkCode");

Expand Down
6 changes: 6 additions & 0 deletions linux/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,9 @@ if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

# get tf lite binaries
install(
FILES ${PROJECT_BUILD_DIR}/../blobs/libtensorflowlite_c-linux.so
DESTINATION ${INSTALL_BUNDLE_DATA_DIR}/../blobs/
)
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ flutter:
- assets/flutter_i18n/zh_CN.yaml
- assets/flutter_i18n/zh_TW.yaml
- assets/Classtable-Empty.png
- assets/captcha-solver.tflite
- assets/captcha-solver-payment.tflite
- assets/captcha-solver-zfw.tflite
# - assets/flutter_i18n/zh_SG.json

6 changes: 6 additions & 0 deletions windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,9 @@ install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

# get tf lite binaries
install(
FILES ${PROJECT_BUILD_DIR}/../blobs/libtensorflowlite_c-win.dll
DESTINATION ${INSTALL_BUNDLE_DATA_DIR}/../blobs/
)