psiphon_mobileproxy 0.0.1 copy "psiphon_mobileproxy: ^0.0.1" to clipboard
psiphon_mobileproxy: ^0.0.1 copied to clipboard

Flutter plugin that runs a Psiphon tunnel on-device behind a local HTTP proxy, so an app can route chosen traffic through Psiphon on Android and iOS without any VPN permission.

example/lib/main.dart

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

import 'package:flutter/material.dart';
import 'package:psiphon_mobileproxy/psiphon_mobileproxy.dart';

void main() {
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Psiphon Mobileproxy',
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF1565C0),
        useMaterial3: true,
      ),
      home: const ProxyDemoPage(),
    );
  }
}

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

  @override
  State<ProxyDemoPage> createState() => _ProxyDemoPageState();
}

class _ProxyDemoPageState extends State<ProxyDemoPage>
    with WidgetsBindingObserver {
  final _psiphon = PsiphonMobileproxy();
  final _configController = TextEditingController();

  String _platformVersion = 'Unknown';
  ProxyInfo? _proxy;
  bool _busy = false;
  String _log = '';

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _psiphon.getPlatformVersion().then((version) {
      if (mounted) setState(() => _platformVersion = version ?? 'unknown');
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _configController.dispose();
    // Not awaited: dispose is synchronous, and the plugin also tears the
    // tunnel down when the engine detaches.
    unawaited(_psiphon.stop());
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // Releasing the tunnel on detach is the habit worth copying into a real
    // app: until it is stopped, Psiphon holds a connection open and keeps
    // using battery and data.
    if (state == AppLifecycleState.detached) {
      unawaited(_psiphon.stop());
    }
  }

  void _appendLog(String message) {
    final timestamp = TimeOfDay.now().format(context);
    setState(() => _log = '[$timestamp] $message\n$_log');
  }

  Future<void> _start() async {
    final config = _configController.text.trim();
    if (config.isEmpty) {
      _appendLog('Paste a Psiphon config first — see the README.');
      return;
    }

    setState(() => _busy = true);
    _appendLog('Connecting the tunnel, this can take a while...');
    try {
      final proxy = await _psiphon.start(psiphonConfig: config);
      if (!mounted) return;
      setState(() => _proxy = proxy);
      _appendLog('Started proxy at ${proxy.address}');
    } on TunnelTimeoutException catch (e) {
      _appendLog('Timed out: ${e.message}');
    } on PsiphonMobileproxyException catch (e) {
      _appendLog('Failed to start: ${e.message}');
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  Future<void> _stop() async {
    setState(() => _busy = true);
    try {
      await _psiphon.stop();
      if (!mounted) return;
      setState(() => _proxy = null);
      _appendLog('Stopped the proxy and disconnected the tunnel');
    } on PsiphonMobileproxyException catch (e) {
      _appendLog('Failed to stop: ${e.message}');
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  Future<void> _testConnection() async {
    final proxy = _proxy;
    if (proxy == null) return;

    setState(() => _busy = true);
    _appendLog('Fetching https://example.com through ${proxy.address} ...');
    final httpClient = HttpClient();
    try {
      httpClient.findProxy = (uri) => 'PROXY ${proxy.address}';

      final request = await httpClient
          .getUrl(Uri.parse('https://example.com'))
          .timeout(const Duration(seconds: 30));
      final response =
          await request.close().timeout(const Duration(seconds: 30));
      await response.drain<void>();
      _appendLog('Success: HTTP ${response.statusCode}');
    } catch (e) {
      _appendLog('Request failed: $e');
    } finally {
      httpClient.close();
      if (mounted) setState(() => _busy = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final isRunning = _proxy != null;

    return Scaffold(
      appBar: AppBar(title: const Text('Psiphon Mobileproxy Demo')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text('Running on $_platformVersion',
              style: theme.textTheme.bodySmall),
          const SizedBox(height: 16),
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Psiphon config', style: theme.textTheme.titleMedium),
                  const SizedBox(height: 4),
                  Text(
                    'The JSON document issued to you by the Psiphon team. This '
                    'package cannot ship one; see the README for how to '
                    'request it.',
                    style: theme.textTheme.bodySmall,
                  ),
                  const SizedBox(height: 8),
                  TextField(
                    controller: _configController,
                    enabled: !isRunning,
                    minLines: 3,
                    maxLines: 6,
                    style: const TextStyle(fontFamily: 'monospace'),
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      hintText: '{"PropagationChannelId": "...", ...}',
                    ),
                  ),
                  const SizedBox(height: 16),
                  Row(
                    children: [
                      Expanded(
                        child: FilledButton(
                          onPressed: _busy || isRunning ? null : _start,
                          child: const Text('Start'),
                        ),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: OutlinedButton(
                          onPressed: _busy || !isRunning ? null : _stop,
                          child: const Text('Stop'),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          Card(
            color: isRunning
                ? theme.colorScheme.primaryContainer
                : theme.colorScheme.surfaceContainerHighest,
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  Icon(isRunning ? Icons.check_circle : Icons.circle_outlined),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Text(
                      isRunning ? 'Running at ${_proxy!.address}' : 'Stopped',
                    ),
                  ),
                  if (isRunning)
                    TextButton(
                      onPressed: _busy ? null : _testConnection,
                      child: const Text('Test connection'),
                    ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          Text('Log', style: theme.textTheme.titleMedium),
          const SizedBox(height: 8),
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: theme.colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(8),
            ),
            child: Text(_log.isEmpty ? 'No activity yet.' : _log),
          ),
        ],
      ),
    );
  }
}
0
likes
150
points
63
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter plugin that runs a Psiphon tunnel on-device behind a local HTTP proxy, so an app can route chosen traffic through Psiphon on Android and iOS without any VPN permission.

Repository (GitHub)
View/report issues

Topics

#psiphon #proxy #networking #privacy #censorship

License

GPL-3.0 (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on psiphon_mobileproxy

Packages that implement psiphon_mobileproxy