Search code examples
flutterhttpsflutter-inappwebview

flutter_inappwebview local https connection refused in flutter app


I'm trying to serve local content from assets through https, in order to gain access to features like webrtc which require ssl.

Since the local app server provided in flutter_inappwebview does not handle ssl connections, I've replaced the InAppLocalHostServer class with InAppLocalHostSecureServer with the following code:

import 'dart:io';
import 'dart:async';

import 'package:flutter/services.dart' show rootBundle;
import 'package:mime/mime.dart';

class InAppLocalHostSecureServer {
  HttpServer _server;
  int _port = 8443;

  InAppLocalHostSecureServer({int port = 8443}) {
    this._port = port;
  }

  ///Starts a server on http://localhost:[port]/.
  ///
  ///**NOTE for iOS**: For the iOS Platform, you need to add the `NSAllowsLocalNetworking` key with `true` in the `Info.plist` file (See [ATS Configuration Basics](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW35)):
  ///```xml
  ///<key>NSAppTransportSecurity</key>
  ///<dict>
  ///    <key>NSAllowsLocalNetworking</key>
  ///    <true/>
  ///</dict>
  ///```
  ///The `NSAllowsLocalNetworking` key is available since **iOS 10**.
  Future<void> start() async {
    if (this._server != null) {
      throw Exception('Server already started on https://localhost:$_port');
    }

    var completer = Completer();

    runZoned(() async {
      SecurityContext context = new SecurityContext();
      var chain = await rootBundle.load('assets/certificates/cert.pem');
      var key = await rootBundle.load('assets/certificates/key.pem');
      context.useCertificateChainBytes(chain.buffer.asInt8List());
      context.usePrivateKeyBytes(key.buffer.asInt8List(), password: 'dartdart');

      HttpServer.bindSecure('127.0.0.1', _port, context).then((server) {
        print('Server running on https://localhost:' + _port.toString());

        this._server = server;

        server.listen((HttpRequest request) async {
          print(request);
          var body = List<int>();
          var path = request.requestedUri.path;
          path = (path.startsWith('/')) ? path.substring(1) : path;
          path += (path.endsWith('/')) ? 'index.html' : '';

          try {
            body = (await rootBundle.load(path)).buffer.asUint8List();
          } catch (e) {
            print(e.toString());
            request.response.close();
            return;
          }

          var contentType = ['text', 'html'];
          if (!request.requestedUri.path.endsWith('/') &&
              request.requestedUri.pathSegments.isNotEmpty) {
            var mimeType =
                lookupMimeType(request.requestedUri.path, headerBytes: body);
            if (mimeType != null) {
              contentType = mimeType.split('/');
            }
          }

          request.response.headers.contentType =
              ContentType(contentType[0], contentType[1], charset: 'utf-8');
          request.response.add(body);
          request.response.close();
        });

        completer.complete();
      });
    }, onError: (e, stackTrace) {
      print('Error: $e $stackTrace');
    });

    return completer.future;
  }

  ///Closes the server.
  Future<void> close() async {
    if (this._server != null) {
      await this._server.close(force: true);
      print('Server running on http://localhost:$_port closed');
      this._server = null;
    }
  }
}

Most of the code is a copy paste of the original class.

What I changed is that I call HttpServer.bindSecure instead of HttpServer.bind and I provide openssl certificate and key.

The server seems to start without error logged in the console, but I cannot access it.

Here is the client code that try to access a local url:


import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'InAppLocalHostSecureServer.dart';

class WebAudioTest extends StatefulWidget {
  @override
  _WebAudioTestState createState() => _WebAudioTestState();
}

class _WebAudioTestState extends State<WebAudioTest> {
  InAppWebViewController webView;
  InAppLocalHostSecureServer localhostServer;
  String url = "https://127.0.0.1:8443/assets/web/index.html";

  @override
  void initState() {
    super.initState();
    this.init();
  }

  void init() async {
    this.localhostServer = new InAppLocalHostSecureServer();
    await localhostServer.start();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Web Audio Test'),
      ),
      body: InAppWebView(
        initialUrl: url,
        initialHeaders: {},
        initialOptions: InAppWebViewGroupOptions(
            crossPlatform: InAppWebViewOptions(
          debuggingEnabled: true,
        )),
        onWebViewCreated: (InAppWebViewController c) {
          webView = c;
        },
        onConsoleMessage: (controller, consoleMessage) {
          print("CONSOLE MESSAGE: " + consoleMessage.message);
        },
      ),
    );
  }
}

No error appears in the console but the flutter page display the following error message:

net::ERR_CONNECTION_REFUSED

Any help is welcome.


Solution

  • Ok, to answer my own questions:

    the problem I had was simply that I build the InAppWebView too early, before the server has finished to launch. The solution is easy, just set a flag to true when the server is launched, and create the InAppWebView only when the flag is true.

    Beside this, WebRTC works without https on localhost, I tested it on Android and iOS. So no need for local https for this use case.

    But anyway if for any other reason someone needs to to serve https local content, the code in this post can serve as a basis for this.