Skip to content

Commit

Permalink
auth improvements for sankaku, idol, r34hentai
Browse files Browse the repository at this point in the history
  • Loading branch information
NANI-SORE committed Feb 3, 2024
1 parent a031c26 commit 6d0429b
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 472 deletions.
4 changes: 2 additions & 2 deletions lib/src/boorus/booru_type.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ignore_for_file: constant_identifier_names

enum BooruType {
AutoDetect,
Autodetect,
//
AGNPH,
BooruOnRails,
Expand Down Expand Up @@ -43,7 +43,7 @@ enum BooruType {
..remove(BooruType.Downloads)
..remove(BooruType.Favourites)
..remove(BooruType.Merge)
..remove(BooruType.AutoDetect)
..remove(BooruType.Autodetect)
..remove(BooruType.Hydrus);
}

Expand Down
137 changes: 81 additions & 56 deletions lib/src/boorus/idol_sankaku_handler.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import 'dart:convert';

import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';

import 'package:lolisnatcher/src/boorus/sankaku_handler.dart';
import 'package:lolisnatcher/src/data/booru_item.dart';
import 'package:lolisnatcher/src/data/comment_item.dart';
import 'package:lolisnatcher/src/data/constants.dart';
import 'package:lolisnatcher/src/data/tag_type.dart';
import 'package:lolisnatcher/src/handlers/settings_handler.dart';
import 'package:lolisnatcher/src/utils/dio_network.dart';

class IdolSankakuHandler extends SankakuHandler {
IdolSankakuHandler(super.booru, super.limit);
Expand All @@ -16,6 +20,13 @@ class IdolSankakuHandler extends SankakuHandler {
return Options(responseType: ResponseType.plain);
}

@override
Map<String, String> getHeaders() {
return {
'User-Agent': EnvironmentConfig.hasSiSecret ? 'SCChannelApp/4.0 (Android; idol)' : Constants.defaultBrowserUserAgent,
};
}

@override
List parseListFromResponse(dynamic response) {
if (response.data is String && (response.data as String).contains('post-premium-browsing_error')) {
Expand Down Expand Up @@ -92,7 +103,11 @@ class IdolSankakuHandler extends SankakuHandler {
String makeURL(String tags) {
final tagsStr = tags.trim().isEmpty ? '' : 'tags=${tags.trim()}&';

return '${booru.baseURL}/post/index.json?${tagsStr}limit=$limit&page=$pageNum';
final authPart = (booru.userID?.isNotEmpty == true && booru.apiKey?.isNotEmpty == true && login.isNotEmpty && passHash.isNotEmpty && appkey.isNotEmpty)
? 'login=$login&password_hash=$passHash&appkey=$appkey&'
: '';

return '${booru.baseURL}/post/index.json?$authPart${tagsStr}limit=$limit&page=$pageNum';
}

@override
Expand Down Expand Up @@ -135,59 +150,69 @@ class IdolSankakuHandler extends SankakuHandler {
}

@override
Future<String> getAuthToken() async {
return '';
}

// attempt to crack idol's login process, idol.sankakucomplex.com works, but it doesn't carry to iapi. domain
// their own idol app has a login through api, but it uses appkey and password_hash, which I have no way of knowing
// @override
// Future<String> getAuthToken() async {
// if (booru.userID?.isNotEmpty != true || booru.apiKey?.isNotEmpty != true) {
// return '';
// }

// String? sessionCookie, authenticityToken, languageCookie;
// final responseLogin = await DioNetwork.get(
// 'https://idol.sankakucomplex.com/users/login',
// headers: {
// 'User-Agent': Constants.defaultBrowserUserAgent,
// },
// options: Options(responseType: ResponseType.plain),
// );
// if (responseLogin.statusCode == 200) {
// sessionCookie = responseLogin.headers['set-cookie']?.map((e) => e.split(';').first).join(';');
// languageCookie = responseLogin.headers['set-cookie']?.firstWhereOrNull((e) => e.contains('locale'))?.split(';').first;
// if (sessionCookie != null) {
// final document = parse(responseLogin.data);
// authenticityToken = document.querySelector('form input[name=authenticity_token]')?.attributes['value'];
// }
// }
// if (authenticityToken == null) {
// return '';
// }

// final response = await DioNetwork.post(
// 'https://idol.sankakucomplex.com/${languageCookie?.split('=')[1] ?? 'en'}/users/authenticate',
// headers: {
// 'User-Agent': Constants.defaultBrowserUserAgent,
// 'Cookie': '$sessionCookie;',
// },
// options: Options(
// contentType: Headers.formUrlEncodedContentType,
// ),
// data: 'authenticity_token=$authenticityToken&url=&user%5Bname%5D=${booru.userID?.replaceAll('@', '%40')}&user%5Bpassword%5D=${booru.apiKey}&commit=Login',
// );

// String token = '';
// if (response.statusCode == 200 || response.statusCode == 302) {
// Logger.Inst().log('Sankaku idol auth token loaded', className, 'getAuthToken', LogTypes.booruHandlerInfo);
// token = responseLogin.headers['set-cookie']?.map((e) => e.split(';').first).join(';') ?? '';
// }
// if (token == '') {
// Logger.Inst().log('Sankaku auth error ${response.statusCode}', className, 'getAuthToken', LogTypes.booruHandlerInfo);
// }

// return token;
// }
bool get hasSignInSupport => true;

String generateSha1(String str) {
return sha1.convert(utf8.encode(str)).toString();
}

String login = '', appkey = '', passHash = '';

@override
Future<bool> canSignIn() async {
return EnvironmentConfig.hasSiSecret && booru.userID?.isNotEmpty == true && booru.apiKey?.isNotEmpty == true;
}

@override
Future<bool> isSignedIn() async {
return login.isNotEmpty && appkey.isNotEmpty && passHash.isNotEmpty;
}

@override
Future<bool> signIn() async {
login = '';
appkey = '';
passHash = '';

bool success = false;

try {
appkey = generateSha1(EnvironmentConfig.siSecret1.replaceAll('[username]', booru.userID!.toLowerCase()));
passHash = generateSha1(EnvironmentConfig.siSecret2.replaceAll('[password]', booru.apiKey!));

final res = await DioNetwork.post(
'${booru.baseURL}/user/authenticate.json',
headers: {
'Accept-Encoding': 'gzip',
'Cache-Control': 'no-store, no-cache, must-revalidate',
'Connection': 'Keep-Alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'User-Agent': 'SCChannelApp/4.0 (Android; idol)',
},
options: Options(
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
),
data: {
'appkey': appkey,
'login': booru.userID?.toLowerCase(),
'password_hash': passHash,
},
);
if (res.statusCode == 200) {
login = res.data['current_user']?['name'];
appkey = generateSha1(EnvironmentConfig.siSecret1.replaceAll('[username]', login.toLowerCase()));
success = true;
}
} catch (e) {
success = false;
}
return success;
}

@override
Future<void> signOut({bool fromError = false}) async {
login = '';
appkey = '';
passHash = '';
}
}
63 changes: 25 additions & 38 deletions lib/src/boorus/r34hentai_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import 'package:lolisnatcher/src/boorus/booru_type.dart';
import 'package:lolisnatcher/src/boorus/shimmie_handler.dart';
import 'package:lolisnatcher/src/data/booru.dart';
import 'package:lolisnatcher/src/data/booru_item.dart';
import 'package:lolisnatcher/src/data/sign_in.dart';
import 'package:lolisnatcher/src/data/sign_out.dart';
import 'package:lolisnatcher/src/utils/dio_network.dart';
import 'package:lolisnatcher/src/utils/tools.dart';

Expand All @@ -22,17 +20,6 @@ class R34HentaiHandler extends ShimmieHandler {
return tags;
}

@override
Map<String, String> getHeaders() {
final Map<String, String> headers = {
...super.getHeaders(),
if (booru.apiKey?.isNotEmpty == true && (booru.apiKey?.contains('shm_user') == true || booru.apiKey?.contains('shm_sesion') == true))
'Cookie': '${booru.apiKey};',
};

return headers;
}

@override
List parseListFromResponse(dynamic response) {
final document = parse(response.data);
Expand Down Expand Up @@ -87,15 +74,15 @@ class R34HentaiHandler extends ShimmieHandler {
bool get hasSignInSupport => true;

@override
Future<bool> signIn(SignInData data) async {
Future<bool> signIn() async {
final CookieManager cookieManager = CookieManager.instance();
List<String>? setCookies;
try {
final res = await DioNetwork.post(
'${booru.baseURL}/user_admin/login',
data: {
'user': data.login,
'pass': data.password,
'user': booru.userID,
'pass': booru.apiKey,
'gobu': 'Log+In',
},
options: Options(
Expand Down Expand Up @@ -143,36 +130,36 @@ class R34HentaiHandler extends ShimmieHandler {
if (!hasCookies) {
return false;
} else {
// TODO here could be a check if the cookies are still valid/not expired, but webview lib doesn't support expire dates for cookies?
// TODO here could be a check if the cookies are still valid/not expired, but webview lib doesn't support expire dates for cookies? are they just dropping them if expired on next read?
// or maybe do a network request to check if the cookies are still valid?
return true;
}
}

@override
Future<bool?> signOut(SignOutData? data) async {
if (!await isSignedIn()) {
return null;
}

Future<bool?> signOut({bool fromError = false}) async {
bool success = false;
try {
await DioNetwork.get(
'${booru.baseURL}/user_admin/logout',
headers: await Tools.getFileCustomHeaders(
Booru(
'R34Hentai',
BooruType.R34Hentai,
'${booru.baseURL}/favicon.ico',
booru.baseURL,
'',
),
),
);
if (fromError) {
success = true;
} catch (e) {
if (e is DioException) {
success = e.response?.statusCode == 200;
} else {
try {
final res = await DioNetwork.get(
'${booru.baseURL}/user_admin/logout',
headers: await Tools.getFileCustomHeaders(
Booru(
'R34Hentai',
BooruType.R34Hentai,
'${booru.baseURL}/favicon.ico',
booru.baseURL,
'',
),
),
);
success = res.statusCode == 200;
} catch (e) {
if (e is DioException) {
success = false;
}
}
}

Expand Down
73 changes: 40 additions & 33 deletions lib/src/boorus/sankaku_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import 'package:lolisnatcher/src/data/tag_type.dart';
import 'package:lolisnatcher/src/handlers/booru_handler.dart';
import 'package:lolisnatcher/src/utils/dio_network.dart';
import 'package:lolisnatcher/src/utils/logger.dart';
import 'package:lolisnatcher/src/utils/tools.dart';

class SankakuHandler extends BooruHandler {
SankakuHandler(super.booru, super.limit);
Expand Down Expand Up @@ -39,16 +38,10 @@ class SankakuHandler extends BooruHandler {
bool get hasLoadItemSupport => true;

@override
Future<Response<dynamic>> fetchSearch(Uri uri, {bool withCaptchaCheck = true, Map<String, dynamic>? queryParams}) async {
try {
if (authToken == '' && booru.userID?.isNotEmpty == true && booru.apiKey?.isNotEmpty == true) {
authToken = await getAuthToken();
Logger.Inst().log('Got authtoken: $authToken', className, 'fetchSearch', LogTypes.booruHandlerInfo);
}
} catch (e) {
Logger.Inst().log('Failed to get authtoken: $e', className, 'fetchSearch', LogTypes.booruHandlerInfo);
}
bool get hasSignInSupport => true;

@override
Future<Response<dynamic>> fetchSearch(Uri uri, {bool withCaptchaCheck = true, Map<String, dynamic>? queryParams}) async {
return DioNetwork.get(
uri.toString(),
headers: getHeaders(),
Expand Down Expand Up @@ -126,9 +119,7 @@ class SankakuHandler extends BooruHandler {
@override
Future<List> loadItem({required BooruItem item, CancelToken? cancelToken}) async {
try {
if (authToken == '' && booru.userID?.isNotEmpty == true && booru.apiKey?.isNotEmpty == true) {
authToken = await getAuthToken();
}
await searchSetup();
final response = await DioNetwork.get(
makeApiPostURL(item.postURL.split('/').last),
headers: getHeaders(),
Expand Down Expand Up @@ -183,31 +174,47 @@ class SankakuHandler extends BooruHandler {
return '${booru.baseURL}/posts/$id';
}

Future<String> getAuthToken() async {
String token = '';
final response = await DioNetwork.post(
'${booru.baseURL}/auth/token',
queryParameters: {'lang': 'english'},
headers: {
'Content-Type': 'application/json',
'User-Agent': Tools.browserUserAgent,
},
data: {'login': booru.userID, 'password': booru.apiKey},
// encoding: Encoding.getByName("utf-8"),
);
@override
Future<bool> isSignedIn() async {
return authToken.isNotEmpty;
}

@override
Future<bool> signIn() async {
bool success = false;
try {
final response = await DioNetwork.post(
'${booru.baseURL}/auth/token',
queryParameters: {'lang': 'english'},
headers: {
'Content-Type': 'application/json',
'User-Agent': Constants.defaultBrowserUserAgent,
},
data: {'login': booru.userID, 'password': booru.apiKey},
);

if (response.statusCode == 200) {
final Map<String, dynamic> parsedResponse = response.data;
if (parsedResponse['success']) {
Logger.Inst().log('Sankaku auth token loaded', className, 'getAuthToken', LogTypes.booruHandlerInfo);
token = "${parsedResponse["token_type"]} ${parsedResponse["access_token"]}";
if (response.statusCode == 200) {
final Map<String, dynamic> parsedResponse = response.data;
if (parsedResponse['success']) {
authToken = "${parsedResponse["token_type"]} ${parsedResponse["access_token"]}";
}
}
} catch (e) {
Logger.Inst().log('Sankaku auth error: $e', className, 'signIn', LogTypes.booruHandlerInfo);
}
if (token == '') {
Logger.Inst().log('Sankaku auth error ${response.statusCode}', className, 'getAuthToken', LogTypes.booruHandlerInfo);
if (authToken == '') {
Logger.Inst().log('Sankaku auth error: empty token ', className, 'signIn', LogTypes.booruHandlerInfo);
success = false;
} else {
Logger.Inst().log('Sankaku auth Got authtoken: $authToken', className, 'signIn', LogTypes.booruHandlerInfo);
success = true;
}
return success;
}

return token;
@override
Future<void> signOut({bool fromError = false}) async {
authToken = '';
}

@override
Expand Down
Loading

0 comments on commit 6d0429b

Please sign in to comment.