Files
AutoJudge-Flutter/lib/services/aufe_connection.dart

540 lines
16 KiB
Dart
Raw Normal View History

2025-11-13 09:14:49 +08:00
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';
}