I am building a flutter project where I want to make GET requests to a .onion tor address (REST API). I found a good library: (https://pub.dev/packages/utopic_tor_onion_proxy), that allows you to do this, and i am using the dart:io libary to make socket connections. I can successfully make GET requests to adresses like: https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion (duckduckgo).
But the code fails when opening a socket to my own .onion address. This is because i dont have a valid TLS certificate. Is it possible to make a socket connection with the dart:io library without using a TLS certificate?
code im using to make a socket connection (default from the utopic_tor_onion_proxy library):
import 'dart:io';
if (uri.scheme == 'https') {
_socket = await SecureSocket.secure(
_socket!,
host: uri.authority,
);
I want to have an option like:
allowInsecureConnection = true,
The same thing is easily possible with languages like python. For example:
import requests
session = requests.session()
session.proxies = {'https': 'socks5h://localhost:9150'}
r = session.get(url, headers=headers, verify=False)
Where verify=False solves the problem.
With CURL it is possible by adding --insecure
But I can't find a way to do it in flutter.
Here is the code I am using:
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:utopic_tor_onion_proxy/utopic_tor_onion_proxy.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String? _torLocalPort;
String? _error;
String? _responseString;
Socket? _socket;
Future<void> _startTor() async {
String? port;
try {
port = (await UtopicTorOnionProxy.startTor()).toString();
} on Exception catch (e) {
print(e);
_error = 'Failed to get port';
}
if (!mounted) return;
setState(() {
_torLocalPort = port;
});
}
Future<void> _stopTor() async {
try {
if (await (UtopicTorOnionProxy.stopTor() as FutureOr<bool>)) {
if (!mounted) return;
setState(() {
_torLocalPort = null;
});
}
} on PlatformException catch (e) {
print(e.message ?? '');
}
}
Future<void> _sendGetRequest(Uri uri) async {
if (mounted) {
setState(() {
_responseString = null;
});
}
_socket?.destroy();
_socket = await Socket.connect(
InternetAddress.loopbackIPv4,
int.tryParse(_torLocalPort!)!,
timeout: Duration(seconds: 5),
);
_socket!.setOption(SocketOption.tcpNoDelay, true);
_socksConnectionRequest(uri, _socket!);
List<int> responseIntList = [];
void onSocketDone() {
print('socket done');
if (mounted) {
setState(() {
_responseString = String.fromCharCodes(responseIntList);
});
}
}
_socket!.listen((event) async {
if (event.length == 8 && event[0] == 0x00 && event[1] == 0x5B) {
print('Connection open');
if (uri.scheme == 'https') {
_socket = await SecureSocket.secure(
_socket!,
host: uri.authority,
);
_socket!.listen((event) {
responseIntList.addAll(event);
}).onDone(onSocketDone);
}
var requestString = 'GET ${uri.path} HTTP/1.1\r\n'
'Host: ${uri.authority}\r\n\r\n';
_socket!.write(requestString);
return;
}
responseIntList.addAll(event);
}).onDone(onSocketDone);
}
void _socksConnectionRequest(Uri uri, Socket socket) {
var uriPortBytes = [(uri.port >> 8) & 0xFF, uri.port & 0xFF];
var uriAuthorityAscii = ascii.encode(uri.authority);
socket.add([
0x04, // SOCKS version
0x01, // request establish a TCP/IP stream connection
...uriPortBytes, // 2 bytes destination port
0x00, // 4 bytes of destination ip
0x00, // if socks4a and destination ip equals 0.0.0.NonZero
0x00, // then we can pass destination domen after first 0x00 byte
0x01,
0x00,
...uriAuthorityAscii, // destination domen
0x00,
]);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Tor Onion Proxy example'),
),
body: LayoutBuilder(
builder: (context, constrains) {
return Scrollbar(
child: SingleChildScrollView(
child: Container(
constraints: BoxConstraints(minHeight: constrains.maxHeight),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(height: 20),
Text(
'Tor running on: ${_torLocalPort ?? _error ?? 'Unknown'}'),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
runSpacing: 20,
spacing: 20,
children: <Widget>[
OutlinedButton(
child: Text('Start Tor Onion Proxy'),
onPressed:
_torLocalPort == null ? _startTor : null,
),
OutlinedButton(
child: Text('Stop Tor Onion Proxy'),
onPressed:
_torLocalPort != null ? _stopTor : null,
),
OutlinedButton(
child:
Text('Send request to check.torproject.org'),
onPressed: _torLocalPort != null
? () => _sendGetRequest(
Uri.https('xxxxxxx.onion:port', '/REST_CALL/'))
: null,
),
],
),
),
if (_responseString != null)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Response: \n\n$_responseString'),
),
],
),
),
),
);
},
),
),
);
}
@override
void dispose() {
_socket!.close();
super.dispose();
}
}
I got it working. The problem was that I used
xxxxxxx.onion:port. The ':port' was the problem.Let's say the call was xxxxxxx.onion:1111. In the
void _socksConnectionRequest(Uri uri, Socket socket)function, there is a uri.port and a uri.authority. the uri.port will be '1111', but the uri.authority will be xxxxxxx.onion:1111, therefore you have the port two times in the function. You can fix this by replacing:var uriAuthorityAscii = ascii.encode(uri.authority);withvar uriAuthorityAscii = ascii.encode(uri.authority.substring(0, uri.authority.length - 5));in the _socksConnectionRequest function. this will make uri. authority = 'xxxxxxx.onion'. Note that I did -5 here, because our port is :1111, so it has 5 characters. If your port is:111, you must subtract 4. You can always replace this with a function if you have variable port length.onBadCertificate: (_) => truewas still needed in the_socket = await SecureSocket.securefunction. It solved theCERTIFICATE_VERIFY_FAILED: self signed certificate(handshake.cc:393)) error.