Skip to content

Commit cbc9aa4

Browse files
authored
Merge pull request #64 from stalomeow/zfw-captcha
feat: zfw captcha
2 parents 776a24f + acf76d6 commit cbc9aa4

13 files changed

+179
-30
lines changed

.flutter

Submodule .flutter updated 3281 files
125 KB
Binary file not shown.

assets/captcha-solver-zfw.tflite

126 KB
Binary file not shown.

assets/captcha-solver.tflite

-191 KB
Binary file not shown.

blobs/libtensorflowlite_c-linux.so

3 MB
Binary file not shown.
5.45 MB
Binary file not shown.

blobs/libtensorflowlite_c-win.dll

2.04 MB
Binary file not shown.

lib/page/public_widget/captcha_input_dialog.dart

Lines changed: 123 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,149 @@
44
// A captcha input dialog.
55

66
import 'dart:typed_data';
7+
import 'dart:math';
78

89
import 'package:flutter/material.dart';
910
import 'package:flutter_i18n/flutter_i18n.dart';
1011
import 'package:watermeter/page/public_widget/toast.dart';
1112
import 'package:image/image.dart' as img;
1213
import 'package:tflite_flutter/tflite_flutter.dart';
1314

15+
enum DigitCaptchaType { payment, zfw }
16+
1417
class DigitCaptchaClientProvider {
15-
static const String _interpreterAssetName = 'assets/captcha-solver.tflite';
18+
// Ref: https://github.com/stalomeow/captcha-solver
19+
20+
static String _getInterpreterAssetName(DigitCaptchaType type) {
21+
return 'assets/captcha-solver-${type.name.toLowerCase()}.tflite';
22+
}
23+
24+
static double _lerp(double a, double b, double t) {
25+
return a + (b - a) * t;
26+
}
27+
28+
static num _sampleMin(img.Image image, List<int> bb, double u, double v) {
29+
int x = _lerp(bb[0] * 1.0, bb[2] - 1.0, u).floor();
30+
int y = _lerp(bb[1] * 1.0, bb[3] - 1.0, v).floor();
31+
num px = min(image.getPixelClamped(x, y + 0).r,
32+
image.getPixelClamped(x + 1, y + 0).r);
33+
num py = min(image.getPixelClamped(x, y + 1).r,
34+
image.getPixelClamped(x + 1, y + 1).r);
35+
return min(px, py);
36+
}
37+
38+
static List<int> _getbbox(img.Image image) {
39+
int left = image.width;
40+
int upper = image.height;
41+
int right = 0; // Exclusive
42+
int lower = 0; // Exclusive
1643

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

48+
// Binarization
49+
if (p < 0.98) {
50+
continue;
51+
}
52+
53+
left = min(left, x);
54+
upper = min(upper, y);
55+
right = max(right, x + 1);
56+
lower = max(lower, y + 1);
57+
}
58+
}
59+
60+
// Expand the bounding box by 1 pixel
61+
left = max(0, left - 1);
62+
upper = max(0, upper - 1);
63+
right = min(image.width, right + 1);
64+
lower = min(image.height, lower + 1);
65+
66+
return [left, upper, right, lower];
67+
}
68+
69+
static img.Image? _getImage(DigitCaptchaType type, List<int> imageData) {
2070
img.Image image = img.decodeImage(Uint8List.fromList(imageData))!;
2171
image = img.grayscale(image);
2272
image = image.convert(
2373
format: img.Format.float32, numChannels: 1); // 0-256 to 0-1
2474

75+
if (type == DigitCaptchaType.zfw) {
76+
// Invert the image
77+
for (int x = 0; x < image.width; x++) {
78+
for (int y = 0; y < image.height; y++) {
79+
image.setPixelR(x, y, 1.0 - image.getPixel(x, y).r);
80+
}
81+
}
82+
83+
List<int> bb = _getbbox(image);
84+
85+
// The numbers are too close
86+
if (bb[2] - bb[0] < 44) {
87+
return null;
88+
}
89+
90+
// Align with the size of payment captcha
91+
img.Image result = new img.Image(
92+
width: 200, height: 80, format: img.Format.float32, numChannels: 1);
93+
for (int x = 0; x < result.width; x++) {
94+
for (int y = 0; y < result.height; y++) {
95+
double u = x * 1.0 / result.width;
96+
double v = y * 1.0 / result.height;
97+
num r = _sampleMin(image, bb, u, v);
98+
result.setPixelR(x, y, r);
99+
}
100+
}
101+
image = result;
102+
}
103+
104+
return image;
105+
}
106+
107+
static int _argmax(List<double> list) {
108+
int result = 0;
109+
for (int i = 1; i < list.length; i++) {
110+
if (list[i] > list[result]) {
111+
result = i;
112+
}
113+
}
114+
return result;
115+
}
116+
117+
static int _getClassCount(DigitCaptchaType type) {
118+
if (type == DigitCaptchaType.payment) {
119+
return 9; // The payment captcha only contains number 1-9
120+
}
121+
return 10;
122+
}
123+
124+
static int _getClassLabel(DigitCaptchaType type, int klass) {
125+
if (type == DigitCaptchaType.payment) {
126+
return klass + 1; // The payment captcha only contains number 1-9
127+
}
128+
return klass;
129+
}
130+
131+
static Future<String?> infer(
132+
DigitCaptchaType type, List<int> imageData) async {
133+
img.Image? image = _getImage(type, imageData);
134+
135+
if (image == null) {
136+
return null;
137+
}
138+
25139
int dim2 = image.height;
26140
int dim3 = image.width ~/ 4;
141+
int classCount = _getClassCount(type);
27142

28143
var input = List.filled(dim2 * dim3, 0.0)
29144
.reshape<double>([1, dim2, dim3, 1]) as List<List<List<List<double>>>>;
30-
var output =
31-
List.filled(9, 0.0).reshape<double>([1, 9]) as List<List<double>>;
145+
var output = List.filled(classCount, 0.0).reshape<double>([1, classCount])
146+
as List<List<double>>;
32147

33-
final interpreter = await Interpreter.fromAsset(_interpreterAssetName);
148+
final interpreter =
149+
await Interpreter.fromAsset(_getInterpreterAssetName(type));
34150
List<int> nums = [];
35151

36152
// Four numbers
@@ -42,21 +158,11 @@ class DigitCaptchaClientProvider {
42158
}
43159

44160
interpreter.run(input, output);
45-
nums.add(_argmax(output[0]) + 1);
161+
nums.add(_getClassLabel(type, _argmax(output[0])));
46162
}
47163

48164
return nums.join('');
49165
}
50-
51-
static int _argmax(List<double> list) {
52-
int result = 0;
53-
for (int i = 1; i < list.length; i++) {
54-
if (list[i] > list[result]) {
55-
result = i;
56-
}
57-
}
58-
return result;
59-
}
60166
}
61167

62168
class CaptchaInputDialog extends StatelessWidget {

lib/repository/schoolnet_session.dart

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,36 @@ class SchoolnetSession extends NetworkSession {
9797
if (csrf.isEmpty || key.isEmpty) throw NotInitalizedException;
9898

9999
String lastErrorMessage = "";
100-
for (int retry = 5; retry > 0; retry--) {
100+
for (int retry = 10; retry > 0; retry--) {
101+
// Refresh captcha
102+
await _dio
103+
.get('https://zfw.xidian.edu.cn/site/captcha', queryParameters: {
104+
'refresh': 1,
105+
'_': DateTime.now().millisecondsSinceEpoch,
106+
});
101107
// Get verifycode
102-
String imgPath = parse(page.data.toString())
103-
.getElementById("loginform-verifycode-image")
104-
?.attributes["src"] ??
105-
"";
106108
var picture = await _dio
107-
.get(imgPath, options: Options(responseType: ResponseType.bytes))
109+
.get(
110+
"https://zfw.xidian.edu.cn/site/captcha",
111+
options: Options(responseType: ResponseType.bytes),
112+
)
108113
.then((data) => data.data);
109114

110-
String verifycode = retry == 1
115+
String? verifycode = retry == 1
111116
? captchaFunction != null
112117
? await captchaFunction(picture)
113118
: throw CaptchaFailedException() // The last try
114-
: await DigitCaptchaClientProvider.infer(picture);
119+
: await DigitCaptchaClientProvider.infer(
120+
DigitCaptchaType.zfw, picture);
121+
122+
if (verifycode == null) {
123+
log.info(
124+
'[SchoolnetSession] Captcha is impossible to be inferred.');
125+
retry++; // Do not count this try
126+
continue;
127+
}
128+
129+
log.info("[SchoolnetSession] verifycode is $verifycode");
115130

116131
// Encrypt the password
117132
var rsaKey = RSAKeyParser().parse(key);
@@ -140,9 +155,16 @@ class SchoolnetSession extends NetworkSession {
140155
if (!jsonDecode(page.data)["success"]) {
141156
lastErrorMessage = jsonDecode(page.data)["message"] ?? "未知错误";
142157
log.info(
143-
"[SchoolNetSession] Attempt ${5 - retry} "
158+
"[SchoolNetSession] Attempt ${11 - retry} "
144159
"failed: $lastErrorMessage",
145160
);
161+
162+
// No need to retry if the error is about username or password
163+
if (lastErrorMessage.contains("用户名") ||
164+
lastErrorMessage.contains("密码")) {
165+
break;
166+
}
167+
146168
continue;
147169
}
148170
// Login post

lib/repository/xidian_ids/payment_session.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,11 +369,19 @@ xh5zeF9usFgtdabgACU/cQIDAQAB
369369
)
370370
.then((data) => data.data);
371371

372-
String checkCode = retry == 1
372+
String? checkCode = retry == 1
373373
? captchaFunction != null
374374
? await captchaFunction(picture)
375375
: throw CaptchaFailedException() // The last try
376-
: await DigitCaptchaClientProvider.infer(picture);
376+
: await DigitCaptchaClientProvider.infer(
377+
DigitCaptchaType.payment, picture);
378+
379+
if (checkCode == null) {
380+
log.info(
381+
'[PaymentSession][getOwe] Captcha is impossible to be inferred.');
382+
retry++; // Do not count this try
383+
continue;
384+
}
377385

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

0 commit comments

Comments
 (0)