Zekfad / nhentai_dart

Dart client for nhentai.net undocumented APIs
https://pub.dev/packages/nhentai
ISC License
10 stars 1 forks source link

403 (Forbidden) error on data request #9

Closed WeebNetsu closed 1 year ago

WeebNetsu commented 1 year ago

I tried using the example you provided in the readme:

final api = API();

    try {
      /// Throws if book is not found, or parse failed, see docs.
      final Book book = await api.getBook(177013);
    } on ApiClientException catch (e) {
      debugPrint(e.response?.statusCode.toString());
    }

I seem to be getting a 403 error response. This is currently only running in a basic Dart project , no Flutter or anything else

It is making a request to https://nhentai.net/api/gallery/177013 which when visited in browser seems to work fine

Zekfad commented 1 year ago

Host is protected via cloudflare, you must provide valid cf_clearance cookie and user agent to make it work. See advanced example: https://github.com/Zekfad/nhentai_dart/blob/984ce5ef3e8b872a6b8b36851158699b9f992862/example/cookie.dart#L12-L20

WeebNetsu commented 1 year ago

Thank you for your reply!

After getting the cookies and setting the user agent, I still have this issue... Here i printed out some of the details from the error. It seems to be similar to #2 where it is loading cloudflare. SInce I provided a (valid) cookie, should it not skip it?

originalException:

FormatException: Unexpected character (at character 1)
<!DOCTYPE html>
^

message: Bad response type

reasonPhrase: Forbidden

headers:

{connection: close, cache-control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, transfer-encoding: chunked, date: Sun, 16 Jul 2023 11:05:35 GMT, content-encoding: gzip, vary: Accept-Encoding, permissions-policy: accelerometer=(),autoplay=(),camera=(),clipboard-read=(),clipboard-write=(),geolocation=(),gyroscope=(),hid=(),interest-cohort=(),magnetometer=(),microphone=(),payment=(),publickey-credentials-get=(),screen-wake-lock=(),serial=(),sync-xhr=(),usb=(), referrer-policy: same-origin, report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=uX%2Fbic%2F63TztEwQ%2FP8t3Evk%2FpvXylkm5KkOrGI3IMFFvEcd62RbjJWigSBhIFmAOK3Cb%2FhkoMa6iRs2VGuOpKjxAKq3YudBqr6J5RRJ140pS9Vl1ovYZ%2FGfDQ2qL"}],"group":"cf-nel","max_age":604800}, cross-origin-opener-policy: same-origin, content-type: text/html; charset=UTF-8, cross-origin-embedder-policy: require-corp, server: cloudflare, cf-mitigated: challenge, cross-origin-resource-policy: same-origin, nel: {"success_fr

statusCode: 403

request

method: GET

headers:

{cookie: cf_clearance=3L1OOZ1qLd70K3zRhFQXz76iq_IuPx_l7yC9ff1jkCs-1689503608-0-250; HttpOnly; csrftoken=cP03hL0aevSvgzYs9ErsvDpRxWr9r5Am6EoDEofkrAlVBlt5oObMT6b1mX4o4XvH; HttpOnly}

url: https://nhentai.net/api/gallery/177013

Here is my user agent I got from fk_user_agent, if it might be important: Dalvik/2.1.0 (Linux; U; Android 13; sdk_gphone_x86_64 Build/TE1A.220922.025)

WeebNetsu commented 1 year ago

After looking at your example again, could it be that I have to use a proxy?

Zekfad commented 1 year ago

The condition behind cf_clearance is single token corresponds to single User Agent and IP address, meaning if you obtain token from browser, you must use browser's User Agent (or complete challenge by setting browser's UA to module's).

From what I know, you could use WebView to implement cookie grabber.

Proxy from my example comes from the fact that the site is blocked in the country.

WeebNetsu commented 1 year ago

YES! Thank you my friend, so turns out the issue was that the user agent used by flutter_webview was not the same user agent returned by fk_user_agent, which caused it to continuously fail the check!

For anyone else who might be in a similar bind in the future, here is a snippet of my solution:

WebView:

import 'package:fk_user_agent/fk_user_agent.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:nclientv3/constants/constants.dart';
import 'package:nclientv3/models/cook.dart';
import 'package:nclientv3/utils/utils.dart';
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
import 'package:webview_flutter/webview_flutter.dart';

class NotARobotView extends StatefulWidget {
  const NotARobotView({super.key});

  static route() => MaterialPageRoute(
        builder: (context) => const NotARobotView(),
      );

  @override
  State<NotARobotView> createState() => _NotARobotViewState();
}

class _NotARobotViewState extends State<NotARobotView> {
  @override
  void initState() {
    super.initState();
  }

  final controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setBackgroundColor(const Color(0x00000000))
    ..setNavigationDelegate(
      NavigationDelegate(
        onPageStarted: (String url) async {
          final currentUserAgent = await WebViewController().runJavaScriptReturningResult('navigator.userAgent');

          // NOTE THIS! You need to save this value to use as your user agent!!!
          debugPrint(currentUserAgent.toString());
        },
        onUrlChange: (change) async {
          if (change.url == NHentaiConstants.url) {
            final realCookieManager = WebviewCookieManager();
            final gotCookies = await realCookieManager.getCookies(NHentaiConstants.url);

            // save your cookies (cooks is a global variable used for testing)
            cooks = gotCookies;
            saveCookieData(gotCookies);
          }
        },
      ),
    )
    ..loadRequest(Uri.parse(NHentaiConstants.url));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: WebViewWidget(controller: controller),
    );
  }
}

Component contacting API:

import 'package:fk_user_agent/fk_user_agent.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:nclientv3/models/cook.dart';
import 'package:nclientv3/utils/utils.dart';
import 'package:nhentai/before_request_add_cookies.dart';
import 'package:nhentai/nhentai.dart' as nh;

class BrowseView extends StatefulWidget {
  const BrowseView({super.key});

  static route() => MaterialPageRoute(
        builder: (context) => const BrowseView(),
      );

  @override
  State<BrowseView> createState() => _BrowseViewState();
}

class _BrowseViewState extends State<BrowseView> {
  @override
  void initState() {
    super.initState();
  }

  void setNotRobot() {
    Navigator.pushNamed(context, "/not-a-robot");
  }

  void getData() async {
    final t = await loadCookieData();

    if (t == null || t.isEmpty) {
      return setNotRobot();
    }

    final api = nh.API(
      userAgent:
      // NOTE HERE: You need to pass in the user agent you got inside your webview
          "Mozilla/5.0 (Linux; Android 13; sdk_gphone_x86_64 Build/TE1A.220922.025; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/103.0.5060.71 Mobile Safari/537.36",
      beforeRequest: beforeRequestAddCookiesStatic(
        // the cookies (cooks - global variable that I stored the cookies in)
        cooks,
      ),
    );

    try {
      /// Throws if book is not found, or parse failed, see docs.
      final nh.Book book = await api.getBook(177013);

      // Short book summary
      debugPrint('Book: $book\n');
    } on nh.ApiClientException catch (e) {
      debugPrint('url ${e.request?.url}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 20),
            child: Column(
              children: [
                MaterialButton(
                  onPressed: getData,
                  child: const Text('Get data'),
                ),
                MaterialButton(
                  onPressed: setNotRobot,
                  child: const Text('Fetch Tokens'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Thanks a ton for your help and patience @Zekfad !

Zekfad commented 1 year ago

You're welcome!