Skip to content

Commit

Permalink
Merge pull request #488 from DanXi-Dev/webvpn-fix
Browse files Browse the repository at this point in the history
fix: webvpn login
  • Loading branch information
ivanfei-1 authored Jan 23, 2025
2 parents 0d6717a + a884ae0 commit 411c12c
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 13 deletions.
9 changes: 8 additions & 1 deletion lib/util/io/dio_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import 'package:dan_xi/provider/settings_provider.dart';
import 'package:dan_xi/util/platform_universal.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';

import '../../repository/cookie/independent_cookie_jar.dart';

/// Useful utils when processing network requests with dio.
class DioUtils {
Expand Down Expand Up @@ -52,7 +55,8 @@ class DioUtils {
/// Thus, some necessary headers (e.g. cookies) will be missing from the second
/// request and on.
static Future<Response<dynamic>> processRedirect(
Dio dio, Response<dynamic> response) async {
Dio dio, Response<dynamic> response,
[IndependentCookieJar? jar]) async {
// Prevent the redirect being processed by HttpClient, with the 302 response caught manually.
if (response.statusCode == 302 &&
response.headers['location'] != null &&
Expand All @@ -62,6 +66,9 @@ class DioUtils {
if (!Uri.parse(location).isAbsolute) {
location = '${response.requestOptions.uri.origin}/$location';
}
if (jar != null) {
dio.interceptors.add(CookieManager(jar));
}
return processRedirect(dio,
await dio.get(location, options: NON_REDIRECT_OPTION_WITH_FORM_TYPE));
} else {
Expand Down
131 changes: 119 additions & 12 deletions lib/util/webvpn_proxy.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import 'package:beautiful_soup_dart/beautiful_soup.dart';
import 'package:dan_xi/model/person.dart';
import 'package:dan_xi/provider/settings_provider.dart';
import 'package:dan_xi/repository/fdu/uis_login_tool.dart';
import 'package:dan_xi/repository/cookie/independent_cookie_jar.dart';
import 'package:dan_xi/repository/cookie/readonly_cookie_jar.dart';
import 'package:dan_xi/repository/forum/forum_repository.dart';
import 'package:dan_xi/util/io/dio_utils.dart';
import 'package:dan_xi/util/platform_universal.dart';
import 'package:dan_xi/util/public_extension_methods.dart';
import 'package:dio/dio.dart';
import 'package:dio5_log/interceptor/diox_log_interceptor.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';

import 'io/queued_interceptor.dart';
import 'io/user_agent_interceptor.dart';

class WebvpnRequestException implements Exception {
final String? message;

Expand All @@ -30,10 +40,7 @@ class WebvpnProxy {
// Cookies related with webvpn
static final ReadonlyCookieJar webvpnCookieJar = ReadonlyCookieJar();

static const String DIRECT_CONNECT_TEST_URL = "https://danta.fudan.edu.cn";

static const String WEBVPN_LOGIN_URL =
"https://uis.fudan.edu.cn/authserver/login?service=https%3A%2F%2Fwebvpn.fudan.edu.cn%2Flogin%3Fcas_login%3Dtrue";
static const String DIRECT_CONNECT_TEST_URL = "https://forum.fudan.edu.cn";

static final Map<String, String> _vpnPrefix = {
"www.fduhole.com":
Expand Down Expand Up @@ -119,21 +126,121 @@ class WebvpnProxy {
debugPrint("Logging into WebVPN");

try {
// Temporary cookie jar
IndependentCookieJar workJar = IndependentCookieJar();
loginSession =
UISLoginTool.loginUIS(dio, WEBVPN_LOGIN_URL, workJar, _personInfo);
await loginSession;
// Clone from temp jar to our dedicated webvpn jar
webvpnCookieJar.cloneFrom(workJar);
isLoggedIn = true;
// init idDio
Dio idDio = DioUtils.newDioWithProxy();
idDio.options = BaseOptions(
receiveDataWhenStatusError: true,
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
sendTimeout: const Duration(seconds: 5));
idDio.interceptors.add(UserAgentInterceptor(
userAgent: SettingsProvider.getInstance().customUserAgent));
IndependentCookieJar idJar = IndependentCookieJar();
idDio.interceptors.add(CookieManager(idJar));

Response<String> firstResponse = await idDio.get(
"https://id.fudan.edu.cn/idp/authCenter/authenticate?service=https%3A%2F%2Fwebvpn.fudan.edu.cn%2Flogin%3Fcas_login%3Dtrue",
options: DioUtils.NON_REDIRECT_OPTION_WITH_FORM_TYPE);

String? firstTicket = _retrieveTicket(firstResponse);

if (firstTicket != null) {
await _performWebvpnLogin(firstTicket);
return;
}

final Response<dynamic> _ =
await DioUtils.processRedirect(idDio, firstResponse, idJar);

final location = await _authenticateWithUIS();
final Response<dynamic> responseWithTicket = await idDio.get(location);

String? ticket = _retrieveTicket(responseWithTicket);
if (ticket == null) {
throw Exception("Failed to retrieve ticket from id.fudan.edu.cn");
}
await _performWebvpnLogin(ticket);
} finally {
loginSession = null;
}
// Any exception thrown won't be catched and will be propagated to widgets
}
}

static Future<void> _performWebvpnLogin(String ticket) async {
Dio webvpnDio = DioUtils.newDioWithProxy();
IndependentCookieJar webvpnJar = IndependentCookieJar();
webvpnDio.interceptors.add(CookieManager(webvpnJar));
Map<String, dynamic> queryParams = {
'cas_login': 'true',
'ticket': ticket,
};
String webvpnUrl = "https://webvpn.fudan.edu.cn/login";

final redirectResponse = await webvpnDio.get(webvpnUrl,
queryParameters: queryParams,
options: DioUtils.NON_REDIRECT_OPTION_WITH_FORM_TYPE);
final Response<dynamic> response =
await DioUtils.processRedirect(webvpnDio, redirectResponse, webvpnJar);

if (response.statusCode != 200) {
throw Exception(
"Failed to log in to WebVPN, status code: ${response.statusCode}");
}
webvpnCookieJar.cloneFrom(webvpnJar);
isLoggedIn = true;
}

// w568w's [UISLoginTool.loginUIS] and [UISLoginTool.tryAsyncWithAuth] are not reusable and comprehensible in any case ;(
// so I rewrite this function here
static Future<String> _authenticateWithUIS() async {
IndependentCookieJar uisJar = IndependentCookieJar();
Dio uisDio = DioUtils.newDioWithProxy();
uisDio.options = BaseOptions(
receiveDataWhenStatusError: true,
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
sendTimeout: const Duration(seconds: 5));
uisDio.interceptors.add(LimitedQueuedInterceptor.getInstance());
uisDio.interceptors.add(UserAgentInterceptor(
userAgent: SettingsProvider.getInstance().customUserAgent));
uisDio.interceptors.add(CookieManager(uisJar));
uisDio.interceptors.add(DioLogInterceptor());
Map<String?, String?> data = {};
Response<String> res = await uisDio.get(
"https://uis.fudan.edu.cn/authserver/login?service=https://id.fudan.edu.cn/idp/thirdAuth/cas");
BeautifulSoup(res.data!).findAll("input").forEach((element) {
if (element.attributes['type'] != "button") {
data[element.attributes['name']] = element.attributes['value'];
}
});
data['username'] = _personInfo!.id;
data["password"] = _personInfo?.password;
res = await uisDio.post(
"https://uis.fudan.edu.cn/authserver/login?service=https://id.fudan.edu.cn/idp/thirdAuth/cas",
data: data.encodeMap(),
options: DioUtils.NON_REDIRECT_OPTION_WITH_FORM_TYPE);

return res.headers['location']![0];
}

static String? _retrieveTicket(Response<dynamic> response) {
if (response.realUri.host != "id.fudan.edu.cn") {
return null;
}

BeautifulSoup soup = BeautifulSoup(response.data!);

for (final element in soup.findAll('input')) {
if (element.attributes['name'] == 'ticket') {
return element.attributes['value'];
}
}

// Return null if no matching element or attribute is found
return null;
}

/// Check if we are able to connect to the service directly (without WebVPN).
/// This method uses a low-timeout dio to reduce wait time.
static Future<bool> tryDirect<T>() async {
Expand Down

0 comments on commit 411c12c

Please sign in to comment.