Files
AutoJudge-Flutter/lib/services/aufe_connection.dart
2025-11-13 09:14:49 +08:00

540 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:crypto/crypto.dart';
import 'package:pointycastle/export.dart';
import '../models/login_status.dart';
import '../models/course.dart';
import 'http_client.dart';
import '../utils/retry_handler.dart';
/// AUFE教务系统连接类
class AUFEConnection {
final String userId;
final String ecPassword;
final String password;
late HTTPClient _client;
String? _twfId;
String? _token;
bool _ecLogged = false;
bool _uaapLogged = false;
DateTime _lastCheck = DateTime.now();
// 配置常量
static const String serverUrl = 'https://vpn.aufe.edu.cn';
static const String uaapLoginUrl =
'http://uaap-aufe-edu-cn.vpn2.aufe.edu.cn:8118/cas/login?service=http%3A%2F%2Fjwcxk2.aufe.edu.cn%2Fj_spring_cas_security_check';
static const String uaapCheckUrl =
'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/';
static const String ecCheckUrl =
'http://txzx-aufe-edu-cn-s.vpn2.aufe.edu.cn:8118/dzzy/list.htm';
AUFEConnection({
required this.userId,
required this.ecPassword,
required this.password,
});
/// 初始化HTTP客户端
void startClient() {
_client = HTTPClient(baseUrl: serverUrl, timeout: 30000);
}
/// EC系统登录RSA加密
Future<ECLoginStatus> ecLogin() async {
try {
return await RetryHandler.retry(
operation: () async => await _performEcLogin(),
retryIf: RetryHandler.shouldRetryOnError,
maxAttempts: 3,
);
} catch (e) {
return ECLoginStatus(failUnknownError: true);
}
}
Future<ECLoginStatus> _performEcLogin() async {
try {
// 1. 获取认证参数
final response = await _client.get('/por/login_auth.csp?apiversion=1');
final responseText = response.data.toString();
// 2. 提取TwfID
final twfIdMatch = RegExp(
r'<TwfID>(.*?)</TwfID>',
).firstMatch(responseText);
if (twfIdMatch == null) {
return ECLoginStatus(failNotFoundTwfid: true);
}
_twfId = twfIdMatch.group(1);
// 3. 提取RSA密钥
final rsaKeyMatch = RegExp(
r'<RSA_ENCRYPT_KEY>(.*?)</RSA_ENCRYPT_KEY>',
).firstMatch(responseText);
if (rsaKeyMatch == null) {
return ECLoginStatus(failNotFoundRsaKey: true);
}
final rsaKey = rsaKeyMatch.group(1)!;
// 4. 提取RSA指数
final rsaExpMatch = RegExp(
r'<RSA_ENCRYPT_EXP>(.*?)</RSA_ENCRYPT_EXP>',
).firstMatch(responseText);
if (rsaExpMatch == null) {
return ECLoginStatus(failNotFoundRsaExp: true);
}
final rsaExp = rsaExpMatch.group(1)!;
// 5. 提取CSRF代码
final csrfMatch = RegExp(
r'<CSRF_RAND_CODE>(.*?)</CSRF_RAND_CODE>',
).firstMatch(responseText);
if (csrfMatch == null) {
return ECLoginStatus(failNotFoundCsrfCode: true);
}
final csrfCode = csrfMatch.group(1)!;
// 6. RSA加密密码
final passwordToEncrypt = '${ecPassword}_$csrfCode';
final encryptedPassword = _rsaEncrypt(passwordToEncrypt, rsaKey, rsaExp);
// 7. 执行登录
final loginResponse = await _client.post(
'/por/login_psw.csp?anti_replay=1&encrypt=1&type=cs',
data: {
'svpn_rand_code': '',
'mitm': '',
'svpn_req_randcode': csrfCode,
'svpn_name': userId,
'svpn_password': encryptedPassword,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
headers: {'Cookie': 'TWFID=$_twfId'},
),
);
final loginResponseText = loginResponse.data.toString();
// 8. 检查登录结果
if (loginResponseText.contains('<Result>1</Result>')) {
_client.setCookie('TWFID', _twfId!);
_ecLogged = true;
return ECLoginStatus(success: true);
} else if (loginResponseText.contains('Invalid username or password!')) {
return ECLoginStatus(failInvalidCredentials: true);
} else if (loginResponseText.contains('[CDATA[maybe attacked]]') ||
loginResponseText.contains('CAPTCHA required')) {
return ECLoginStatus(failMaybeAttacked: true);
} else {
return ECLoginStatus(failUnknownError: true);
}
} on DioException catch (e) {
return ECLoginStatus(failNetworkError: true);
} catch (e) {
return ECLoginStatus(failUnknownError: true);
}
}
/// RSA加密
String _rsaEncrypt(String plaintext, String modulusHex, String exponentStr) {
// 解析模数和指数
final modulus = BigInt.parse(modulusHex, radix: 16);
final exponent = BigInt.parse(exponentStr);
// 创建RSA公钥
final publicKey = RSAPublicKey(modulus, exponent);
// 创建加密器
final encryptor = PKCS1Encoding(RSAEngine());
encryptor.init(true, PublicKeyParameter<RSAPublicKey>(publicKey));
// 加密
final plainBytes = utf8.encode(plaintext);
final encrypted = encryptor.process(Uint8List.fromList(plainBytes));
// 转换为十六进制字符串
return encrypted.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
}
/// UAAP系统登录DES加密
Future<UAAPLoginStatus> uaapLogin() async {
try {
return await RetryHandler.retry(
operation: () async => await _performUaapLogin(),
retryIf: RetryHandler.shouldRetryOnError,
maxAttempts: 3,
);
} catch (e) {
return UAAPLoginStatus(failUnknownError: true);
}
}
Future<UAAPLoginStatus> _performUaapLogin() async {
try {
// 1. 获取登录页面
final response = await _client.get(uaapLoginUrl);
final responseText = response.data.toString();
// 2. 提取lt参数
final ltMatch = RegExp(
r'name="lt" value="(.*?)"',
).firstMatch(responseText);
if (ltMatch == null) {
return UAAPLoginStatus(failNotFoundLt: true);
}
final ltValue = ltMatch.group(1)!;
// 3. 提取execution参数
final executionMatch = RegExp(
r'name="execution" value="(.*?)"',
).firstMatch(responseText);
if (executionMatch == null) {
return UAAPLoginStatus(failNotFoundExecution: true);
}
final executionValue = executionMatch.group(1)!;
// 4. DES加密密码
final encryptedPassword = _desEncrypt(password, ltValue);
// 5. 提交登录表单
final loginResponse = await _client.post(
uaapLoginUrl,
data: {
'username': userId,
'password': encryptedPassword,
'lt': ltValue,
'execution': executionValue,
'_eventId': 'submit',
'submit': 'LOGIN',
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
followRedirects: false,
validateStatus: (status) => status! < 500,
),
);
// 6. 检查登录结果并访问重定向URL以建立session
if (loginResponse.statusCode == 302) {
final location = loginResponse.headers['location']?.first ?? '';
print('🔐 UAAP redirect location: $location');
if (location.contains('ticket=')) {
// 访问重定向URL以完成CAS认证并建立session
print('🔐 Following redirect to establish session...');
final ticketResponse = await _client.get(
location,
options: Options(
followRedirects: true,
validateStatus: (status) => status! < 500,
),
);
print('🔐 Ticket response status: ${ticketResponse.statusCode}');
_uaapLogged = true;
return UAAPLoginStatus(success: true);
}
}
final loginResponseText = loginResponse.data.toString();
if (loginResponseText.contains('Invalid username or password')) {
return UAAPLoginStatus(failInvalidCredentials: true);
}
return UAAPLoginStatus(failUnknownError: true);
} on DioException catch (e) {
return UAAPLoginStatus(failNetworkError: true);
} catch (e) {
return UAAPLoginStatus(failUnknownError: true);
}
}
/// DES加密使用TripleDES ECB模式
String _desEncrypt(String plaintext, String key) {
// 处理密钥 - 取前8字节
var keyBytes = utf8.encode(key);
if (keyBytes.length > 8) {
keyBytes = keyBytes.sublist(0, 8);
} else if (keyBytes.length < 8) {
// 不足8字节用0填充
keyBytes = Uint8List(8)..setRange(0, keyBytes.length, keyBytes);
}
// 创建DES密钥TripleDES使用相同的8字节密钥重复3次
final desKey = KeyParameter(
Uint8List(24)
..setRange(0, 8, keyBytes)
..setRange(8, 16, keyBytes)
..setRange(16, 24, keyBytes),
);
// 创建加密器
final cipher = PaddedBlockCipherImpl(PKCS7Padding(), DESedeEngine());
cipher.init(true, PaddedBlockCipherParameters(desKey, null));
// 加密
final plainBytes = utf8.encode(plaintext);
final encrypted = cipher.process(Uint8List.fromList(plainBytes));
// Base64编码
return base64.encode(encrypted);
}
/// 检查EC登录状态
Future<ECCheckStatus> checkEcLoginStatus() async {
if (!_ecLogged) {
return ECCheckStatus(loggedIn: false);
}
try {
final response = await _client.get(ecCheckUrl);
if (response.statusCode == 200) {
return ECCheckStatus(loggedIn: true);
} else {
return ECCheckStatus(loggedIn: false);
}
} on DioException catch (e) {
return ECCheckStatus(failNetworkError: true);
} catch (e) {
return ECCheckStatus(failUnknownError: true);
}
}
/// 检查UAAP登录状态
Future<ECCheckStatus> checkUaapLoginStatus() async {
return ECCheckStatus(loggedIn: _uaapLogged);
}
/// 健康检查
Future<bool> healthCheck() async {
final delta = DateTime.now().difference(_lastCheck);
// 5分钟未检查则视为不健康
if (delta.inSeconds > 300) {
return false;
}
// 检查UAAP登录状态
final uaapStatus = await checkUaapLoginStatus();
if (!uaapStatus.isLoggedIn) {
return false;
}
// 检查EC登录状态
final ecStatus = await checkEcLoginStatus();
if (!ecStatus.isLoggedIn) {
return false;
}
return true;
}
/// 更新健康检查时间戳
void healthCheckpoint() {
_lastCheck = DateTime.now();
}
/// 关闭连接
Future<void> close() async {
_client.close();
}
/// 获取HTTP客户端
HTTPClient get client {
healthCheckpoint();
return _client;
}
/// 获取CSRF Token
Future<String?> getToken() async {
try {
final response = await _client.get(
'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/student/teachingEvaluation/evaluation/index',
);
if (response.statusCode != 200) {
return null;
}
final html = response.data.toString();
final tokenMatch = RegExp(
r'id="tokenValue"[^>]*value="([^"]*)"',
).firstMatch(html);
if (tokenMatch != null) {
_token = tokenMatch.group(1);
return _token;
}
return null;
} catch (e) {
return null;
}
}
/// 获取待评课程列表
Future<List<Course>> fetchCourseList() async {
try {
final response = await _client.post(
'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/student/teachingEvaluation/teachingEvaluation/search?sf_request_type=ajax',
data: {'optType': '1', 'pagesize': '50'},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (response.statusCode != 200) {
return [];
}
final data = response.data;
if (data is Map<String, dynamic> && data['data'] is List) {
final courseList = (data['data'] as List)
.map((item) => _parseCourse(item))
.where((course) => course != null)
.cast<Course>()
.toList();
return courseList;
}
return [];
} catch (e) {
return [];
}
}
/// 解析课程数据
Course? _parseCourse(dynamic item) {
try {
if (item is! Map<String, dynamic>) return null;
final id = item['id'] as Map<String, dynamic>?;
final questionnaire = item['questionnaire'] as Map<String, dynamic>?;
return Course(
id: id?['evaluatedPeople']?.toString() ?? '',
name: item['evaluationContent']?.toString() ?? '',
teacher: item['evaluatedPeople']?.toString() ?? '',
evaluatedPeople: item['evaluatedPeople']?.toString() ?? '',
evaluatedPeopleNumber: id?['evaluatedPeople']?.toString() ?? '',
coureSequenceNumber: id?['coureSequenceNumber']?.toString() ?? '',
evaluationContentNumber:
id?['evaluationContentNumber']?.toString() ?? '',
questionnaireCode:
questionnaire?['questionnaireNumber']?.toString() ?? '',
questionnaireName:
questionnaire?['questionnaireName']?.toString() ?? '',
isEvaluated: item['isEvaluated']?.toString() == '',
);
} catch (e) {
return null;
}
}
/// 访问评价页面并返回HTML内容
Future<String?> accessEvaluationPage(
Course course, {
int? totalCourses,
}) async {
try {
print('📝 Accessing evaluation page for: ${course.name}');
if (_token == null) {
print('📝 Getting token first...');
await getToken();
print('📝 Token: $_token');
}
// count is the total number of courses in the course list
final count = totalCourses?.toString() ?? '28';
print('📝 Using count: $count (total courses: $totalCourses)');
print('📝 Posting to evaluation page...');
final response = await _client.post(
'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/student/teachingEvaluation/teachingEvaluation/evaluationPage',
data: {
'count': count,
'evaluatedPeople': course.evaluatedPeople,
'evaluatedPeopleNumber': course.evaluatedPeopleNumber,
'questionnaireCode': course.questionnaireCode,
'questionnaireName': course.questionnaireName,
'coureSequenceNumber': course.coureSequenceNumber,
'evaluationContentNumber': course.evaluationContentNumber,
'evaluationContentContent': '',
'tokenValue': _token ?? '',
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
print('📝 Access evaluation page status: ${response.statusCode}');
if (response.statusCode == 200) {
final html = response.data.toString();
print('📝 Got HTML content, length: ${html.length}');
return html;
}
return null;
} catch (e, stackTrace) {
print('❌ accessEvaluationPage error: $e');
print('❌ Stack trace: $stackTrace');
return null;
}
}
/// 提交评价表单
Future<EvaluationResponse> submitEvaluation(
Map<String, String> formData,
) async {
try {
print('📤 Submitting evaluation with form data:');
print('📤 Form data entries: ${formData.length}');
formData.forEach((key, value) {
print(
' $key = ${value.length > 50 ? value.substring(0, 50) + "..." : value}',
);
});
final response = await _client.post(
'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/student/teachingEvaluation/teachingEvaluation/assessment?sf_request_type=ajax',
data: formData,
options: Options(contentType: Headers.formUrlEncodedContentType),
);
print('📤 Submit response status: ${response.statusCode}');
print('📤 Submit response data: ${response.data}');
if (response.statusCode != 200) {
return EvaluationResponse(
result: 'error',
msg: '网络请求失败 (${response.statusCode})',
);
}
final data = response.data;
if (data is Map<String, dynamic>) {
return EvaluationResponse(
result: data['result']?.toString() ?? 'error',
msg: data['msg']?.toString() ?? '未知错误',
);
}
return EvaluationResponse(result: 'error', msg: '响应格式错误');
} catch (e) {
print('❌ Submit evaluation error: $e');
return EvaluationResponse(result: 'error', msg: '请求异常: $e');
}
}
}
/// 评价响应
class EvaluationResponse {
final String result;
final String msg;
EvaluationResponse({required this.result, required this.msg});
bool get isSuccess => result == 'success';
}