Skip to content
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

Update ischool login method with retry mechanism #225

Merged
merged 25 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fbed2e9
iSchool login method change
umeow0716 Jan 26, 2024
8b349f7
add ischool login test
James-Lu-none Jan 27, 2024
286fc4b
remove outdated I-School retry loop
James-Lu-none Jan 27, 2024
ee19b64
add session out_of_date error
James-Lu-none Jan 27, 2024
7bb6ad2
add retry mechanis, for getLoginOAuth
James-Lu-none Jan 27, 2024
3bb8c48
remove unused import from test
James-Lu-none Feb 16, 2024
d88ff18
remove unused parameter
James-Lu-none Feb 16, 2024
ddcd27c
rewrite login methods
James-Lu-none Feb 16, 2024
d5fb896
add getSSOIndexResponse function
James-Lu-none Feb 16, 2024
ec437bc
remove unnecessary redirection
James-Lu-none Feb 16, 2024
e5c566a
update comments
James-Lu-none Feb 16, 2024
4c02ed7
dart format
James-Lu-none Feb 16, 2024
7c2b99a
add retry mechanism in oauth server request
James-Lu-none Feb 16, 2024
93c7956
update comments
James-Lu-none Feb 16, 2024
a790a9c
refactoring the retry logic
James-Lu-none Feb 18, 2024
68c30d3
making logEventToFirebase optional
James-Lu-none Feb 18, 2024
6de2398
convert to named argument
James-Lu-none Feb 18, 2024
ff2f9bc
remove unnecessary else statement
James-Lu-none Feb 18, 2024
f69f2c3
rewrite login retry mechanism on step 2
James-Lu-none Feb 18, 2024
aa3b705
add log
James-Lu-none Feb 18, 2024
a2aa391
extend ischool connection state
James-Lu-none Feb 21, 2024
6ba6660
add error handling
James-Lu-none Feb 21, 2024
6aa53b5
update comment
James-Lu-none Feb 21, 2024
bf23aa0
dart format
James-Lu-none Feb 21, 2024
23ff926
Merge branch 'master' into update_ischool_login_method_with_retry
James-Lu-none Feb 28, 2024
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release

# Test files
**/test/ischool_plus_connector_test/credential.json
118 changes: 56 additions & 62 deletions lib/src/connector/ischool_plus_connector.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// TODO: remove sdk version selector after migrating to null-safety.
// @dart=2.10
import 'dart:convert';
import 'dart:developer';
import 'dart:io';

import 'package:dio/dio.dart';
Expand All @@ -17,7 +18,7 @@ import '../model/course/course_student.dart';
import 'core/connector_parameter.dart';
import 'ntut_connector.dart';

enum ISchoolPlusConnectorStatus { loginSuccess, loginFail, unknownError }
enum ISchoolPlusConnectorStatus { loginSuccess, loginGetSSOIndexError, loginRedirectionError, unknownError }

enum IPlusReturnStatus { success, fail, noPermission }

Expand All @@ -38,80 +39,73 @@ class ISchoolPlusConnector {

/// The Authorization Step of ISchool (2023-10-21)
/// 1. GET https://app.ntut.edu.tw/ssoIndex.do
/// 2. POST https://app.ntut.edu.tw/oauth2Server.do (It should be. See the comment on step 2)
/// 3. GET https://istudy.ntut.edu.tw/login2.php (It should be. See the comment on step 3)
/// 4. do something...
static Future<ISchoolPlusConnectorStatus> login(String account) async {
String result;
/// 2-1. POST https://app.ntut.edu.tw/oauth2Server.do (It should be. See the comment on step 2-1)
/// 2-2. follow the redirection to https://istudy.ntut.edu.tw/login2.php (It should be. See the comment on step 2-2)
static Future<ISchoolPlusConnectorStatus> login(String account, {bool logEventToFirebase = true}) async {
try {
ConnectorParameter parameter;
html.Document tagNode;
List<html.Element> nodes;
final data = {
"apUrl": "https://istudy.ntut.edu.tw/login.php",
"apOu": "ischool_plus_oauth",
"sso": "true",
"datetime1": DateTime.now().millisecondsSinceEpoch.toString()
};
final ssoIndexResponse = await getSSOIndexResponse();
if (ssoIndexResponse.isEmpty) return ISchoolPlusConnectorStatus.loginGetSSOIndexError;

// Step 1
parameter = ConnectorParameter(_ssoLoginUrl);
parameter.data = data;
result = (await Connector.getDataByGet(parameter));
final ssoIndexTagNode = html.parse(ssoIndexResponse);
final ssoIndexNodes = ssoIndexTagNode.getElementsByTagName("input");
final ssoIndexJumpUrl = ssoIndexTagNode.getElementsByTagName("form")[0].attributes["action"];

tagNode = html.parse(result.toString().trim());
nodes = tagNode.getElementsByTagName("input");
data.clear();
for (final node in nodes) {
final Map<String, String> oauthData = {};
for (final node in ssoIndexNodes) {
final name = node.attributes['name'];
final value = node.attributes['value'];
data[name] = value;
oauthData[name] = value;
}

// Step 2
// The `jumpUrl` should be "oauth2Server.do".
// If not, it means that the school server has changed.
// TODO: Add a validation measurement to check whether Step 1 is died or not. (It should not die if auth is correct)
final jumpUrl = tagNode.getElementsByTagName("form")[0].attributes["action"];
parameter = ConnectorParameter("${NTUTConnector.host}$jumpUrl");
parameter.data = data;

Response<dynamic> jumpResult = (await Connector.getDataByPostResponse(parameter));
tagNode = html.parse(jumpResult.data.toString().trim());
nodes = tagNode.getElementsByTagName("a");

// Step 3
// The redirectUrl is provided by <a> HTML DOM on Step 2.
// It should be https://istudy.ntut.edu.tw/login2.php with lot of the parameters.
final redirectUrl = nodes.first.attributes["href"];
parameter = ConnectorParameter(redirectUrl);
await Connector.getDataByGet(parameter);

// Perform retry for cryptic API errors (?).
// If the string `connect lost` be found in the response, we will do the retry.

// [2023-10-21] We may not need this since the step was changed.
// TODO: Remove I-School retry loop since it's outdated.

int retryTimes = 3;
do {
if (jumpResult.data.toString().contains('connect lost')) {
// Take a short delay to avoid being blocked.
for (int retry = 0; retry < 3; retry++) {
// Step 2-1
// The ssoIndexJumpUrl should be "oauth2Server.do", and the response should contain redirection location.
// If not, a retry of getting redirection location will perform.
final jumpParameter = ConnectorParameter("${NTUTConnector.host}$ssoIndexJumpUrl");
jumpParameter.data = oauthData;
final jumpResult = (await Connector.getDataByPostResponse(jumpParameter));
if (jumpResult.statusCode != 302) {
log("[TAT] ischool_plus_connector.dart: failed to get redirection location from oauth2Server, retrying...");
await Future.delayed(const Duration(milliseconds: 100));
jumpResult = (await Connector.getDataByPostResponse(parameter));
} else {
break;
continue;
}
} while ((retryTimes--) > 0);
// Step 2-2
// The redirect location should be "https://istudy.ntut.edu.tw/login2.php", and the response should not contain
// "connection `lost`", if it does, a retry of getting redirection location will perform.
final login2Parameter = ConnectorParameter(jumpResult.headers['location'][0]);
final login2Result = await Connector.getDataByGet(login2Parameter);
if (login2Result.contains("lost")) {
log("[TAT] ischool_plus_connector.dart: connection lost during redirection, retrying...");
await Future.delayed(const Duration(milliseconds: 100));
continue;
}
return ISchoolPlusConnectorStatus.loginSuccess;
}

await FirebaseAnalytics.instance.logLogin(
loginMethod: 'ntut_iplus',
);
return ISchoolPlusConnectorStatus.loginSuccess;
if (logEventToFirebase) {
await FirebaseAnalytics.instance.logLogin(
loginMethod: 'ntut_iplus',
);
}
return ISchoolPlusConnectorStatus.loginRedirectionError;
} catch (e, stack) {
Log.eWithStack(e.toString(), stack);
return ISchoolPlusConnectorStatus.loginFail;
rethrow;
}
}

static Future<String> getSSOIndexResponse() async {
final data = {"apOu": "ischool_plus_oauth", "datetime1": DateTime.now().millisecondsSinceEpoch.toString()};
for (int retry = 0; retry < 5; retry++) {
final parameter = ConnectorParameter(_ssoLoginUrl);
parameter.data = data;

final response = (await Connector.getDataByGet(parameter)).toString().trim();
if (response.contains("ssoForm")) return response;
log("[TAT] ischool_plus_connector.dart: failed to get ssoForm, retrying...");
await Future.delayed(const Duration(milliseconds: 100));
}
return "";
}

static Future<ReturnWithStatus<List<CourseStudent>>> getCourseStudent(String courseId) async {
Expand Down
10 changes: 8 additions & 2 deletions lib/src/task/iplus/iplus_system_task.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ class IPlusSystemTask<T> extends NTUTTask<T> {
final value = await ISchoolPlusConnector.login(studentId);
super.onEnd();

if (value != ISchoolPlusConnectorStatus.loginSuccess) {
return onError(R.current.loginISchoolPlusError);
//TODO: generate string for this
switch (value) {
case ISchoolPlusConnectorStatus.loginGetSSOIndexError:
return onError("ischool login get SSO index error");
case ISchoolPlusConnectorStatus.loginRedirectionError:
return onError("ischool login redirection error");
default:
break;
}
}
return status;
Expand Down
62 changes: 62 additions & 0 deletions test/ischool_plus_connector_test/ischool_plus_connector_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// TODO: remove sdk version selector after migrating to null-safety.
// @dart=2.10
import 'dart:io';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:flutter_app/src/connector/blocked_cookies.dart';
import 'package:flutter_app/src/connector/core/dio_connector.dart';
import 'package:flutter_app/src/connector/interceptors/request_interceptor.dart';
import 'package:flutter_app/src/connector/ischool_plus_connector.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:path/path.dart';
import 'package:tat_core/tat_core.dart';
import 'dart:developer' as dev;
import 'dart:convert';

Future<void> main() async {
final tempDir = await Directory.systemTemp.createTemp();

final appDocDir = join(tempDir.path, '.cookies');
final CookieJar cookieJar = PersistCookieJar(storage: FileStorage('$appDocDir/.cookies'));
Get.put(cookieJar);
final apiInterceptors = [
ResponseCookieFilter(blockedCookieNamePatterns: blockedCookieNamePatterns),
CookieManager(cookieJar),
RequestInterceptors(),
];
await DioConnector.instance.init(interceptors: apiInterceptors);
final schoolApiService = SchoolApiService(interceptors: apiInterceptors);

final simpleLoginRepository = SimpleLoginRepository(apiService: schoolApiService);
// final checkSessionRepository = CheckSessionRepository(apiService: schoolApiService);

final simpleLoginUseCase = SimpleLoginUseCase(simpleLoginRepository);
// final checkSessionIsAliveUseCase = CheckSessionUseCase(checkSessionRepository);

const credentialFilePath = 'test/ischool_plus_connector_test/credential.json';
final file = File(credentialFilePath);
final json = jsonDecode(await file.readAsString());
final userId = json['userId'];
final password = json['password'];

dev.log('userId: $userId');
dev.log('password: $password');

final Stopwatch stopwatch = Stopwatch()..start();
test('ntut_login', () async {
final loginCredential = LoginCredential(userId: userId, password: password);
final loginResult = await simpleLoginUseCase(credential: loginCredential);
expect(loginResult.isSuccess, isTrue);
dev.log('ntut login Done Test execution time: ${stopwatch.elapsed}');

// final isCurrentSessionAlive = await checkSessionIsAliveUseCase();
// expect(isCurrentSessionAlive, isTrue);
});
test('ischool_login', () async {
final result = await ISchoolPlusConnector.login(userId, logEventToFirebase: false);
expect(result, ISchoolPlusConnectorStatus.loginSuccess);
dev.log('ischool login Done Test execution time: ${stopwatch.elapsed}');
stopwatch.stop();
});
}
Loading