Initial commit

This commit is contained in:
Nate Brown 2020-07-27 15:43:58 -05:00
commit b546dd1c9d
134 changed files with 10907 additions and 0 deletions

47
.gitignore vendored Normal file
View File

@ -0,0 +1,47 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
/nebula/local.settings
/nebula/MobileNebula.framework/
/nebula/mobileNebula-sources.jar
/nebula/vendor/
/android/app/src/main/libs/mobileNebula.aar
/nebula/mobileNebula.aar
/android/key.properties
/env.sh
/lib/gen.versions.dart
/lib/.gen.versions.dart

10
.metadata Normal file
View File

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
channel: stable
project_type: app

8
android/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
/build/build-attribution/

102
android/app/build.gradle Normal file
View File

@ -0,0 +1,102 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
compileSdkVersion 28
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
applicationId "net.defined.mobile_nebula"
minSdkVersion 25
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['password']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['password']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
// We are disabling minification and proguard because it wrecks the crypto for storing keys
// Ideally we would turn these on. We had issues with gson as well but resolved those with proguardFiles
minifyEnabled false
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
flutter {
source '../..'
}
repositories {
flatDir {
dirs 'src/main/libs'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.security:security-crypto:1.0.0-rc02"
implementation 'com.google.code.gson:gson:2.8.6'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation (name:'mobileNebula', ext:'aar') {
exec {
workingDir '../../'
environment("ANDROID_NDK_HOME", android.ndkDirectory)
environment("ANDROID_HOME", android.sdkDirectory)
commandLine './gen-artifacts.sh', 'android'
}
}
}

11
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,11 @@
# Flutter Wrapper - this came from guidance at https://medium.com/@swav.kulinski/flutter-and-android-obfuscation-8768ac544421
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
# Keep our class names for gson
-keep class net.defined.mobile_nebula.** { *; }
-keep class androidx.security.crypto.** { *; }

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.defined.mobile_nebula">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,49 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.defined.mobile_nebula">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:name="io.flutter.app.FlutterApplication"
android:label="Nebula"
android:icon="@mipmap/ic_launcher">
<service android:name=".NebulaVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:process=":nebulaVpnBg">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
</service>
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -0,0 +1,22 @@
package net.defined.mobile_nebula
import android.content.Context
import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.MasterKeys
import java.io.*
class EncFile(var context: Context) {
private val scheme = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
private val master: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
fun openRead(file: File): BufferedReader {
val eFile = EncryptedFile.Builder(file, context, master, scheme).build()
return eFile.openFileInput().bufferedReader()
}
fun openWrite(file: File): BufferedWriter {
val eFile = EncryptedFile.Builder(file, context, master, scheme).build()
return eFile.openFileOutput().bufferedWriter()
}
}

View File

@ -0,0 +1,402 @@
package net.defined.mobile_nebula
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService
import android.os.*
import androidx.annotation.NonNull;
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
const val TAG = "nebula"
const val VPN_PERMISSIONS_CODE = 0x0F
const val VPN_START_CODE = 0x10
const val CHANNEL = "net.defined.mobileNebula/NebulaVpnService"
class MainActivity: FlutterActivity() {
private var sites: Sites? = null
private var permResult: MethodChannel.Result? = null
private var inMessenger: Messenger? = Messenger(IncomingHandler())
private var outMessenger: Messenger? = null
private var activeSiteId: String? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
//TODO: Initializing in the constructor leads to a context lacking info we need, figure out the right way to do this
sites = Sites(context, flutterEngine)
// Bind against our service to detect which site is running on app boot
val intent = Intent(this, NebulaVpnService::class.java)
bindService(intent, connection, 0)
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when(call.method) {
"android.requestPermissions" -> androidPermissions(result)
"nebula.parseCerts" -> nebulaParseCerts(call, result)
"nebula.generateKeyPair" -> nebulaGenerateKeyPair(result)
"nebula.renderConfig" -> nebulaRenderConfig(call, result)
"listSites" -> listSites(result)
"deleteSite" -> deleteSite(call, result)
"saveSite" -> saveSite(call, result)
"startSite" -> startSite(call, result)
"stopSite" -> stopSite()
"active.listHostmap" -> activeListHostmap(call, result)
"active.listPendingHostmap" -> activeListPendingHostmap(call, result)
"active.getHostInfo" -> activeGetHostInfo(call, result)
"active.setRemoteForTunnel" -> activeSetRemoteForTunnel(call, result)
"active.closeTunnel" -> activeCloseTunnel(call, result)
else -> result.notImplemented()
}
}
}
private fun nebulaParseCerts(call: MethodCall, result: MethodChannel.Result) {
val certs = call.argument<String>("certs")
if (certs == "") {
return result.error("required_argument", "certs is a required argument", null)
}
return try {
val json = mobileNebula.MobileNebula.parseCerts(certs)
result.success(json)
} catch (err: Exception) {
result.error("unhandled_error", err.message, null)
}
}
private fun nebulaGenerateKeyPair(result: MethodChannel.Result) {
val kp = mobileNebula.MobileNebula.generateKeyPair()
return result.success(kp)
}
private fun nebulaRenderConfig(call: MethodCall, result: MethodChannel.Result) {
val config = call.arguments as String
val yaml = mobileNebula.MobileNebula.renderConfig(config, "<hidden>")
return result.success(yaml)
}
private fun listSites(result: MethodChannel.Result) {
sites!!.refreshSites(activeSiteId)
val sites = sites!!.getSites()
val gson = Gson()
val json = gson.toJson(sites)
result.success(json)
}
private fun deleteSite(call: MethodCall, result: MethodChannel.Result) {
val id = call.arguments as String
if (activeSiteId == id) {
stopSite()
}
sites!!.deleteSite(id)
result.success(null)
}
private fun saveSite(call: MethodCall, result: MethodChannel.Result) {
val site: IncomingSite
try {
val gson = Gson()
site = gson.fromJson(call.arguments as String, IncomingSite::class.java)
site.save(context)
} catch (err: Exception) {
//TODO: is toString the best or .message?
return result.error("failure", err.toString(), null)
}
val siteDir = context.filesDir.resolve("sites").resolve(site.id)
try {
// Try to render a full site, if this fails the config was bad somehow
Site(siteDir)
} catch (err: Exception) {
siteDir.deleteRecursively()
return result.error("failure", "Site config was incomplete, please review and try again", null)
}
result.success(null)
}
private fun startSite(call: MethodCall, result: MethodChannel.Result) {
val id = call.argument<String>("id")
if (id == "") {
return result.error("required_argument", "id is a required argument", null)
}
var siteContainer: SiteContainer = sites!!.getSite(id!!) ?: return result.error("unknown_site", "No site with that id exists", null)
siteContainer.site.connected = true
siteContainer.site.status = "Initializing..."
val intent = VpnService.prepare(this)
if (intent != null) {
//TODO: ensure this boots the correct bit, I bet it doesn't and we need to go back to the active symlink
intent.putExtra("path", siteContainer.site.path)
intent.putExtra("id", siteContainer.site.id)
startActivityForResult(intent, VPN_START_CODE)
} else {
val intent = Intent(this, NebulaVpnService::class.java)
intent.putExtra("path", siteContainer.site.path)
intent.putExtra("id", siteContainer.site.id)
onActivityResult(VPN_START_CODE, Activity.RESULT_OK, intent)
}
result.success(null)
}
private fun stopSite() {
val intent = Intent(this, NebulaVpnService::class.java)
intent.putExtra("COMMAND", "STOP")
//This is odd but stopService goes nowhere in my tests and this is correct
// according to the official example https://android.googlesource.com/platform/development/+/master/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java#116
startService(intent)
//TODO: why doesn't this work!?!?
// if (serviceIntent != null) {
// Log.e(TAG, "stopping ${serviceIntent.toString()}")
// stopService(serviceIntent)
// }
}
private fun activeListHostmap(call: MethodCall, result: MethodChannel.Result) {
val id = call.argument<String>("id")
if (id == "") {
return result.error("required_argument", "id is a required argument", null)
}
if (outMessenger == null || activeSiteId == null || activeSiteId != id) {
return result.success(null)
}
var msg = Message.obtain()
msg.what = NebulaVpnService.MSG_LIST_HOSTMAP
msg.replyTo = Messenger(object: Handler() {
override fun handleMessage(msg: Message) {
result.success(msg.data.getString("data"))
}
})
outMessenger?.send(msg)
}
private fun activeListPendingHostmap(call: MethodCall, result: MethodChannel.Result) {
val id = call.argument<String>("id")
if (id == "") {
return result.error("required_argument", "id is a required argument", null)
}
if (outMessenger == null || activeSiteId == null || activeSiteId != id) {
return result.success(null)
}
var msg = Message.obtain()
msg.what = NebulaVpnService.MSG_LIST_PENDING_HOSTMAP
msg.replyTo = Messenger(object: Handler() {
override fun handleMessage(msg: Message) {
result.success(msg.data.getString("data"))
}
})
outMessenger?.send(msg)
}
private fun activeGetHostInfo(call: MethodCall, result: MethodChannel.Result) {
val id = call.argument<String>("id")
if (id == "") {
return result.error("required_argument", "id is a required argument", null)
}
val vpnIp = call.argument<String>("vpnIp")
if (vpnIp == "") {
return result.error("required_argument", "vpnIp is a required argument", null)
}
val pending = call.argument<Boolean>("pending") ?: false
if (outMessenger == null || activeSiteId == null || activeSiteId != id) {
return result.success(null)
}
var msg = Message.obtain()
msg.what = NebulaVpnService.MSG_GET_HOSTINFO
msg.data.putString("vpnIp", vpnIp)
msg.data.putBoolean("pending", pending)
msg.replyTo = Messenger(object: Handler() {
override fun handleMessage(msg: Message) {
result.success(msg.data.getString("data"))
}
})
outMessenger?.send(msg)
}
private fun activeSetRemoteForTunnel(call: MethodCall, result: MethodChannel.Result) {
val id = call.argument<String>("id")
if (id == "") {
return result.error("required_argument", "id is a required argument", null)
}
val vpnIp = call.argument<String>("vpnIp")
if (vpnIp == "") {
return result.error("required_argument", "vpnIp is a required argument", null)
}
val addr = call.argument<String>("addr")
if (vpnIp == "") {
return result.error("required_argument", "addr is a required argument", null)
}
if (outMessenger == null || activeSiteId == null || activeSiteId != id) {
return result.success(null)
}
var msg = Message.obtain()
msg.what = NebulaVpnService.MSG_SET_REMOTE_FOR_TUNNEL
msg.data.putString("vpnIp", vpnIp)
msg.data.putString("addr", addr)
msg.replyTo = Messenger(object: Handler() {
override fun handleMessage(msg: Message) {
result.success(msg.data.getString("data"))
}
})
outMessenger?.send(msg)
}
private fun activeCloseTunnel(call: MethodCall, result: MethodChannel.Result) {
val id = call.argument<String>("id")
if (id == "") {
return result.error("required_argument", "id is a required argument", null)
}
val vpnIp = call.argument<String>("vpnIp")
if (vpnIp == "") {
return result.error("required_argument", "vpnIp is a required argument", null)
}
if (outMessenger == null || activeSiteId == null || activeSiteId != id) {
return result.success(null)
}
var msg = Message.obtain()
msg.what = NebulaVpnService.MSG_CLOSE_TUNNEL
msg.data.putString("vpnIp", vpnIp)
msg.replyTo = Messenger(object: Handler() {
override fun handleMessage(msg: Message) {
result.success(msg.data.getBoolean("data"))
}
})
outMessenger?.send(msg)
}
private fun androidPermissions(result: MethodChannel.Result) {
val intent = VpnService.prepare(this)
if (intent != null) {
permResult = result
return startActivityForResult(intent, VPN_PERMISSIONS_CODE)
}
// We already have the permission
result.success(null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// This is where activity results come back to us (startActivityForResult)
if (requestCode == VPN_PERMISSIONS_CODE && permResult != null) {
// We are processing a response for vpn permissions and the UI is waiting for feedback
//TODO: unlikely we ever register multiple attempts but this could be a trouble spot if we did
val result = permResult!!
permResult = null
if (resultCode == Activity.RESULT_OK) {
return result.success(null)
}
return result.error("denied", "User did not grant permission", null)
} else if (requestCode == VPN_START_CODE) {
// We are processing a response for permissions while starting the VPN (or reusing code in the event we already have perms)
startService(data)
if (outMessenger == null) {
bindService(data, connection, 0)
}
return
}
// The file picker needs us to super
super.onActivityResult(requestCode, resultCode, data)
}
/** Defines callbacks for service binding, passed to bindService() */
val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
outMessenger = Messenger(service)
// We want to monitor the service for as long as we are connected to it.
try {
val msg = Message.obtain(null, NebulaVpnService.MSG_REGISTER_CLIENT)
msg.replyTo = inMessenger
outMessenger?.send(msg)
} catch (e: RemoteException) {
// In this case the service has crashed before we could even
// do anything with it; we can count on soon being
// disconnected (and then reconnected if it can be restarted)
// so there is no need to do anything here.
//TODO:
}
val msg = Message.obtain(null, NebulaVpnService.MSG_IS_RUNNING)
outMessenger?.send(msg)
}
override fun onServiceDisconnected(arg0: ComponentName) {
outMessenger = null
if (activeSiteId != null) {
//TODO: this indicates the service died, notify that it is disconnected
}
activeSiteId = null
}
}
// Handle and route messages coming from the vpn service
inner class IncomingHandler: Handler() {
override fun handleMessage(msg: Message) {
val id = msg.data.getString("id")
//TODO: If the elvis hits then we had a deleted site running, which shouldn't happen
val site = sites!!.getSite(id) ?: return
when (msg.what) {
NebulaVpnService.MSG_IS_RUNNING -> isRunning(site, msg)
NebulaVpnService.MSG_EXIT -> serviceExited(site, msg)
else -> super.handleMessage(msg)
}
}
private fun isRunning(site: SiteContainer, msg: Message) {
var status = "Disconnected"
var connected = false
if (msg.arg1 == 1) {
status = "Connected"
connected = true
}
activeSiteId = site.site.id
site.updater.setState(connected, status)
}
private fun serviceExited(site: SiteContainer, msg: Message) {
activeSiteId = null
site.updater.setState(false, "Disconnected", msg.data.getString("error"))
}
}
}

View File

@ -0,0 +1,209 @@
package net.defined.mobile_nebula
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.VpnService
import android.os.*
import android.util.Log
import mobileNebula.CIDR
import java.io.File
class NebulaVpnService : VpnService() {
companion object {
private const val TAG = "NebulaVpnService"
const val MSG_REGISTER_CLIENT = 1
const val MSG_UNREGISTER_CLIENT = 2
const val MSG_IS_RUNNING = 3
const val MSG_LIST_HOSTMAP = 4
const val MSG_LIST_PENDING_HOSTMAP = 5
const val MSG_GET_HOSTINFO = 6
const val MSG_SET_REMOTE_FOR_TUNNEL = 7
const val MSG_CLOSE_TUNNEL = 8
const val MSG_EXIT = 9
}
/**
* Target we publish for clients to send messages to IncomingHandler.
*/
private lateinit var messenger: Messenger
private val mClients = ArrayList<Messenger>()
private var running: Boolean = false
private var site: Site? = null
private var nebula: mobileNebula.Nebula? = null
private var vpnInterface: ParcelFileDescriptor? = null
//TODO: bindService seems to be how to do IPC
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.getStringExtra("COMMAND") == "STOP") {
stopVpn()
return Service.START_NOT_STICKY
}
startVpn(intent?.getStringExtra("path"), intent?.getStringExtra("id"))
return super.onStartCommand(intent, flags, startId)
}
private fun startVpn(path: String?, id: String?) {
if (running) {
return announceExit(id, "Trying to run nebula but it is already running")
}
//TODO: if we fail to start, android will attempt a restart lacking all the intent data we need.
// Link active site config in Main to avoid this
site = Site(File(path))
var ipNet: CIDR
if (site!!.cert == null) {
return announceExit(id, "Site is missing a certificate")
}
try {
ipNet = mobileNebula.MobileNebula.parseCIDR(site!!.cert!!.cert.details.ips[0])
} catch (err: Exception) {
return announceExit(id, err.message ?: "$err")
}
val builder = Builder()
.addAddress(ipNet.ip, ipNet.maskSize.toInt())
.addRoute(ipNet.network, ipNet.maskSize.toInt())
.setMtu(site!!.mtu)
.setSession(TAG)
// Add our unsafe routes
site!!.unsafeRoutes.forEach { unsafeRoute ->
val ipNet = mobileNebula.MobileNebula.parseCIDR(unsafeRoute.route)
builder.addRoute(ipNet.network, ipNet.maskSize.toInt())
}
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
cm.allNetworks.forEach { network ->
cm.getLinkProperties(network).dnsServers.forEach { builder.addDnsServer(it) }
}
try {
vpnInterface = builder.establish()
nebula = mobileNebula.MobileNebula.newNebula(site!!.config, site!!.getKey(this), site!!.logFile, vpnInterface!!.fd.toLong())
} catch (e: Exception) {
Log.e(TAG, "Got an error $e")
vpnInterface?.close()
announceExit(id, e.message)
return stopSelf()
}
nebula!!.start()
running = true
sendSimple(MSG_IS_RUNNING, if (running) 1 else 0)
}
private fun stopVpn() {
nebula?.stop()
vpnInterface?.close()
running = false
announceExit(site?.id, null)
}
override fun onDestroy() {
stopVpn()
//TODO: wait for the thread to exit
super.onDestroy()
}
private fun announceExit(id: String?, err: String?) {
val msg = Message.obtain(null, MSG_EXIT)
if (err != null) {
msg.data.putString("error", err)
Log.e(TAG, "$err")
}
send(msg, id)
}
/**
* Handler of incoming messages from clients.
*/
inner class IncomingHandler(context: Context, private val applicationContext: Context = context.applicationContext) : Handler() {
override fun handleMessage(msg: Message) {
//TODO: how do we limit what can talk to us?
//TODO: Make sure replyTo is actually a messenger
when (msg.what) {
MSG_REGISTER_CLIENT -> mClients.add(msg.replyTo)
MSG_UNREGISTER_CLIENT -> mClients.remove(msg.replyTo)
MSG_IS_RUNNING -> isRunning()
MSG_LIST_HOSTMAP -> listHostmap(msg)
MSG_LIST_PENDING_HOSTMAP -> listHostmap(msg)
MSG_GET_HOSTINFO -> getHostInfo(msg)
MSG_CLOSE_TUNNEL -> closeTunnel(msg)
MSG_SET_REMOTE_FOR_TUNNEL -> setRemoteForTunnel(msg)
else -> super.handleMessage(msg)
}
}
private fun isRunning() {
sendSimple(MSG_IS_RUNNING, if (running) 1 else 0)
}
private fun listHostmap(msg: Message) {
val res = nebula!!.listHostmap(msg.what == MSG_LIST_PENDING_HOSTMAP)
var m = Message.obtain(null, msg.what)
m.data.putString("data", res)
msg.replyTo.send(m)
}
private fun getHostInfo(msg: Message) {
val res = nebula!!.getHostInfoByVpnIp(msg.data.getString("vpnIp"), msg.data.getBoolean("pending"))
var m = Message.obtain(null, msg.what)
m.data.putString("data", res)
msg.replyTo.send(m)
}
private fun setRemoteForTunnel(msg: Message) {
val res = nebula!!.setRemoteForTunnel(msg.data.getString("vpnIp"), msg.data.getString("addr"))
var m = Message.obtain(null, msg.what)
m.data.putString("data", res)
msg.replyTo.send(m)
}
private fun closeTunnel(msg: Message) {
val res = nebula!!.closeTunnel(msg.data.getString("vpnIp"))
var m = Message.obtain(null, msg.what)
m.data.putBoolean("data", res)
msg.replyTo.send(m)
}
}
private fun sendSimple(type: Int, arg1: Int = 0, arg2: Int = 0) {
send(Message.obtain(null, type, arg1, arg2))
}
private fun sendObj(type: Int, obj: Any?) {
send(Message.obtain(null, type, obj))
}
private fun send(msg: Message, id: String? = null) {
msg.data.putString("id", id ?: site?.id)
mClients.forEach { m ->
try {
m.send(msg)
} catch (e: RemoteException) {
// The client is dead. Remove it from the list;
// we are going through the list from back to front
// so this is safe to do inside the loop.
//TODO: seems bad to remove in loop, double check this is ok
// mClients.remove(m)
}
}
}
override fun onBind(intent: Intent?): IBinder? {
if (intent != null && SERVICE_INTERFACE == intent.action) {
return super.onBind(intent)
}
messenger = Messenger(IncomingHandler(this))
return messenger.binder
}
}

View File

@ -0,0 +1,267 @@
package net.defined.mobile_nebula
import android.content.Context
import android.util.Log
import com.google.gson.Gson
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import java.io.File
import kotlin.collections.HashMap
data class SiteContainer(
val site: Site,
val updater: SiteUpdater
)
class Sites(private var context: Context, private var engine: FlutterEngine) {
private var sites: HashMap<String, SiteContainer> = HashMap()
init {
refreshSites()
}
fun refreshSites(activeSite: String? = null) {
val sitesDir = context.filesDir.resolve("sites")
if (!sitesDir.isDirectory) {
sitesDir.delete()
sitesDir.mkdir()
}
sites = HashMap()
sitesDir.listFiles().forEach { siteDir ->
try {
val site = Site(siteDir)
// Make sure we can load the private key
site.getKey(context)
val updater = SiteUpdater(site, engine)
if (site.id == activeSite) {
updater.setState(true, "Connected")
}
this.sites[site.id] = SiteContainer(site, updater)
} catch (err: Exception) {
siteDir.deleteRecursively()
Log.e(TAG, "Deleting non conforming site ${siteDir.absolutePath}", err)
}
}
}
fun getSites(): Map<String, Site> {
return sites.mapValues { it.value.site }
}
fun deleteSite(id: String) {
sites.remove(id)
val siteDir = context.filesDir.resolve("sites").resolve(id)
siteDir.deleteRecursively()
//TODO: make sure you stop the vpn
//TODO: make sure you relink the active site if this is the active site
}
fun getSite(id: String): SiteContainer? {
return sites[id]
}
}
class SiteUpdater(private var site: Site, engine: FlutterEngine): EventChannel.StreamHandler {
// eventSink is how we send info back up to flutter
private var eventChannel: EventChannel = EventChannel(engine.dartExecutor.binaryMessenger, "net.defined.nebula/${site.id}")
private var eventSink: EventChannel.EventSink? = null
fun setState(connected: Boolean, status: String, err: String? = null) {
site.connected = connected
site.status = status
val d = mapOf("connected" to site.connected, "status" to site.status)
if (err != null) {
eventSink?.error("", err, d)
} else {
eventSink?.success(d)
}
}
init {
eventChannel.setStreamHandler(this)
}
// Methods for EventChannel.StreamHandler
override fun onListen(p0: Any?, p1: EventChannel.EventSink?) {
eventSink = p1
}
override fun onCancel(p0: Any?) {
eventSink = null
}
}
data class CertificateInfo(
@SerializedName("Cert") val cert: Certificate,
@SerializedName("RawCert") val rawCert: String,
@SerializedName("Validity") val validity: CertificateValidity
)
data class Certificate(
val fingerprint: String,
val signature: String,
val details: CertificateDetails
)
data class CertificateDetails(
val name: String,
val notBefore: String,
val notAfter: String,
val publicKey: String,
val groups: List<String>,
val ips: List<String>,
val subnets: List<String>,
val isCa: Boolean,
val issuer: String
)
data class CertificateValidity(
@SerializedName("Valid") val valid: Boolean,
@SerializedName("Reason") val reason: String
)
class Site {
val name: String
val id: String
val staticHostmap: HashMap<String, StaticHosts>
val unsafeRoutes: List<UnsafeRoute>
var cert: CertificateInfo? = null
var ca: Array<CertificateInfo>
val lhDuration: Int
val port: Int
val mtu: Int
val cipher: String
val sortKey: Int
var logVerbosity: String
var connected: Boolean?
var status: String?
val logFile: String?
var errors: ArrayList<String> = ArrayList()
// Path to this site on disk
@Expose(serialize = false)
val path: String
// Strong representation of the site config
@Expose(serialize = false)
val config: String
constructor(siteDir: File) {
val gson = Gson()
config = siteDir.resolve("config.json").readText()
val incomingSite = gson.fromJson(config, IncomingSite::class.java)
path = siteDir.absolutePath
name = incomingSite.name
id = incomingSite.id
staticHostmap = incomingSite.staticHostmap
unsafeRoutes = incomingSite.unsafeRoutes ?: ArrayList()
lhDuration = incomingSite.lhDuration
port = incomingSite.port
mtu = incomingSite.mtu ?: 1300
cipher = incomingSite.cipher
sortKey = incomingSite.sortKey ?: 0
logFile = siteDir.resolve("log").absolutePath
logVerbosity = incomingSite.logVerbosity ?: "info"
connected = false
status = "Disconnected"
try {
val rawDetails = mobileNebula.MobileNebula.parseCerts(incomingSite.cert)
val certs = gson.fromJson(rawDetails, Array<CertificateInfo>::class.java)
if (certs.isEmpty()) {
throw IllegalArgumentException("No certificate found")
}
cert = certs[0]
if (!cert!!.validity.valid) {
errors.add("Certificate is invalid: ${cert!!.validity.reason}")
}
} catch (err: Exception) {
errors.add("Error while loading certificate: ${err.message}")
}
try {
val rawCa = mobileNebula.MobileNebula.parseCerts(incomingSite.ca)
ca = gson.fromJson(rawCa, Array<CertificateInfo>::class.java)
var hasErrors = false
ca.forEach {
if (!it.validity.valid) {
hasErrors = true
}
}
if (hasErrors) {
errors.add("There are issues with 1 or more ca certificates")
}
} catch (err: Exception) {
ca = arrayOf()
errors.add("Error while loading certificate authorities: ${err.message}")
}
}
fun getKey(context: Context): String? {
val f = EncFile(context).openRead(File(path).resolve("key"))
val k = f.readText()
f.close()
return k
}
}
data class StaticHosts(
val lighthouse: Boolean,
val destinations: List<String>
)
data class UnsafeRoute(
val route: String,
val via: String,
val mtu: Int?
)
class IncomingSite(
val name: String,
val id: String,
val staticHostmap: HashMap<String, StaticHosts>,
val unsafeRoutes: List<UnsafeRoute>?,
val cert: String,
val ca: String,
val lhDuration: Int,
val port: Int,
val mtu: Int?,
val cipher: String,
val sortKey: Int?,
var logVerbosity: String?,
@Expose(serialize = false)
var key: String?
) {
fun save(context: Context) {
val siteDir = context.filesDir.resolve("sites").resolve(id)
if (!siteDir.exists()) {
siteDir.mkdir()
}
if (key != null) {
val f = EncFile(context).openWrite(siteDir.resolve("key"))
f.use { it.write(key) }
f.close()
}
key = null
val gson = Gson()
val confFile = siteDir.resolve("config.json")
confFile.writeText(gson.toJson(this))
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="sites" path="."/>
<external-path name="external-sites" path="." />
</paths>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.defined.mobile_nebula">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

31
android/build.gradle Normal file
View File

@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.3.61'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,6 @@
#Fri Jun 05 14:55:48 CDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

15
android/settings.gradle Normal file
View File

@ -0,0 +1,15 @@
include ':app'
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}

View File

@ -0,0 +1 @@
include ':app'

4
env.sh.example Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
# Ensure your go and flutter bin folders are here
export PATH="$PATH:/path/to/go/bin:/path/to/flutter/bin"

Binary file not shown.

51
gen-artifacts.sh Executable file
View File

@ -0,0 +1,51 @@
#!/bin/sh
set -e
. env.sh
# Generate gomobile nebula bindings
cd nebula
if [ "$1" = "ios" ]; then
# Build for nebula for iOS
make MobileNebula.framework
rm -rf ../ios/NebulaNetworkExtension/MobileNebula.framework
cp -r MobileNebula.framework ../ios/NebulaNetworkExtension/
elif [ "$1" = "android" ]; then
# Build nebula for android
make mobileNebula.aar
rm -rf ../android/app/src/main/libs/mobileNebula.aar
cp mobileNebula.aar ../android/app/src/main/libs/mobileNebula.aar
else
echo "Error: unsupported target os $1"
exit 1
fi
cd ..
# Generate version info to display in about
{
# Get the flutter and dart versions
printf "const flutterVersion = <String, String>"
flutter --version --machine
echo ";"
# Get our current git sha
git rev-parse --short HEAD | sed -e "s/\(.*\)/const gitSha = '\1';/"
# Get the nebula version
cd nebula
NEBULA_VERSION="$(go list -m -f "{{.Version}}" github.com/slackhq/nebula | cut -f1 -d'-' | cut -c2-)"
echo "const nebulaVersion = '$NEBULA_VERSION';"
cd ..
# Get our golang version
echo "const goVersion = '$(go version | awk '{print $3}')';"
} > lib/.gen.versions.dart
# Try and avoid issues with building by moving into place after we are complete
#TODO: this might be a parallel build of deps issue in kotlin, might need to solve there
mv lib/.gen.versions.dart lib/gen.versions.dart

33
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
/NebulaNetworkExtension/MobileNebula.framework/

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>8.0</string>
</dict>
</plist>

View File

@ -0,0 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>NebulaNetworkExtension</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.packet-tunnel</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,67 @@
import Foundation
let groupName = "group.net.defined.mobileNebula"
class KeyChain {
class func save(key: String, data: Data) -> Bool {
let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword as String,
kSecAttrAccount as String : key,
kSecValueData as String : data,
kSecAttrAccessGroup as String: groupName,
]
SecItemDelete(query as CFDictionary)
let val = SecItemAdd(query as CFDictionary, nil)
return val == 0
}
class func load(key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword,
kSecAttrAccount as String : key,
kSecReturnData as String : kCFBooleanTrue!,
kSecMatchLimit as String : kSecMatchLimitOne,
kSecAttrAccessGroup as String: groupName,
]
var dataTypeRef: AnyObject? = nil
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == noErr {
return dataTypeRef as! Data?
} else {
return nil
}
}
class func delete(key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword,
kSecAttrAccount as String : key,
kSecReturnData as String : kCFBooleanTrue!,
kSecMatchLimit as String : kSecMatchLimitOne,
kSecAttrAccessGroup as String: groupName,
]
return SecItemDelete(query as CFDictionary) == 0
}
}
extension Data {
init<T>(from value: T) {
var value = value
var data = Data()
withUnsafePointer(to: &value, { (ptr: UnsafePointer<T>) -> Void in
data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1))
})
self.init(data)
}
func to<T>(type: T.Type) -> T {
return self.withUnsafeBytes { $0.load(as: T.self) }
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.net.defined.mobileNebula</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)group.net.defined.mobileNebula</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,174 @@
import NetworkExtension
import MobileNebula
import os.log
import MMWormhole
class PacketTunnelProvider: NEPacketTunnelProvider {
private var networkMonitor: NWPathMonitor?
private var ifname: String?
private var site: Site?
private var _log = OSLog(subsystem: "net.defined.mobileNebula", category: "PacketTunnelProvider")
private var wormhole = MMWormhole(applicationGroupIdentifier: "group.net.defined.mobileNebula", optionalDirectory: "ipc")
private var nebula: MobileNebulaNebula?
private func log(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: _log, args)
}
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
NSKeyedUnarchiver.setClass(IPCRequest.classForKeyedUnarchiver(), forClassName: "Runner.IPCRequest")
let proto = self.protocolConfiguration as! NETunnelProviderProtocol
var config: Data
var key: String
do {
config = proto.providerConfiguration?["config"] as! Data
site = try Site(proto: proto)
} catch {
//TODO: need a way to notify the app
log("Failed to render config from vpn object")
return completionHandler(error)
}
let _site = site!
_log = OSLog(subsystem: "net.defined.mobileNebula:\(_site.name)", category: "PacketTunnelProvider")
do {
key = try _site.getKey()
} catch {
wormhole.passMessageObject(IPCMessage(id: _site.id, type: "error", message: error.localizedDescription), identifier: "nebula")
return completionHandler(error)
}
self.networkMonitor = NWPathMonitor()
self.networkMonitor!.pathUpdateHandler = self.pathUpdate
self.networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
let fileDescriptor = (self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32) ?? -1
if fileDescriptor < 0 {
let msg = IPCMessage(id: _site.id, type: "error", message: "Starting tunnel failed: Could not determine file descriptor")
wormhole.passMessageObject(msg, identifier: "nebula")
return completionHandler(NSError())
}
var ifnameSize = socklen_t(IFNAMSIZ)
let ifnamePtr = UnsafeMutablePointer<CChar>.allocate(capacity: Int(ifnameSize))
ifnamePtr.initialize(repeating: 0, count: Int(ifnameSize))
if getsockopt(fileDescriptor, 2 /* SYSPROTO_CONTROL */, 2 /* UTUN_OPT_IFNAME */, ifnamePtr, &ifnameSize) == 0 {
self.ifname = String(cString: ifnamePtr)
}
ifnamePtr.deallocate()
// This is set to 127.0.0.1 because it has to be something..
let tunnelNetworkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
// Make sure our ip is routed to the tun device
var err: NSError?
let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err)
if (err != nil) {
let msg = IPCMessage(id: _site.id, type: "error", message: err?.localizedDescription ?? "Unknown error from go MobileNebula.ParseCIDR - certificate")
self.wormhole.passMessageObject(msg, identifier: "nebula")
return completionHandler(err)
}
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
var routes: [NEIPv4Route] = [NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)]
// Add our unsafe routes
_site.unsafeRoutes.forEach { unsafeRoute in
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
if (err != nil) {
let msg = IPCMessage(id: _site.id, type: "error", message: err?.localizedDescription ?? "Unknown error from go MobileNebula.ParseCIDR - unsafe routes")
self.wormhole.passMessageObject(msg, identifier: "nebula")
return completionHandler(err)
}
routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR))
}
tunnelNetworkSettings.ipv4Settings!.includedRoutes = routes
tunnelNetworkSettings.mtu = _site.mtu as NSNumber
wormhole.listenForMessage(withIdentifier: "app", listener: self.wormholeListener)
self.setTunnelNetworkSettings(tunnelNetworkSettings, completionHandler: {(error:Error?) in
if (error != nil) {
let msg = IPCMessage(id: _site.id, type: "error", message: error?.localizedDescription ?? "Unknown setTunnelNetworkSettings error")
self.wormhole.passMessageObject(msg, identifier: "nebula")
return completionHandler(error)
}
var err: NSError?
self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, Int(fileDescriptor), &err)
if err != nil {
let msg = IPCMessage(id: _site.id, type: "error", message: err?.localizedDescription ?? "Unknown error from go MobileNebula.Main")
self.wormhole.passMessageObject(msg, identifier: "nebula")
return completionHandler(err)
}
self.nebula!.start()
completionHandler(nil)
})
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
nebula?.stop()
networkMonitor?.cancel()
networkMonitor = nil
completionHandler()
}
private func pathUpdate(path: Network.NWPath) {
nebula?.rebind()
}
private func wormholeListener(msg: Any?) {
guard let call = msg as? IPCRequest else {
log("Failed to decode IPCRequest from network extension")
return
}
var error: Error?
var data: Any?
//TODO: try catch over all this
switch call.type {
case "listHostmap": (data, error) = listHostmap(pending: false)
case "listPendingHostmap": (data, error) = listHostmap(pending: true)
case "getHostInfo": (data, error) = getHostInfo(args: call.arguments!)
case "setRemoteForTunnel": (data, error) = setRemoteForTunnel(args: call.arguments!)
case "closeTunnel": (data, error) = closeTunnel(args: call.arguments!)
default:
error = "Unknown IPC message type \(call.type)"
}
if (error != nil) {
self.wormhole.passMessageObject(IPCMessage(id: "", type: "error", message: error!.localizedDescription), identifier: call.callbackId)
} else {
self.wormhole.passMessageObject(IPCMessage(id: "", type: "success", message: data), identifier: call.callbackId)
}
}
private func listHostmap(pending: Bool) -> (String?, Error?) {
var err: NSError?
let res = nebula!.listHostmap(pending, error: &err)
return (res, err)
}
private func getHostInfo(args: Dictionary<String, Any>) -> (String?, Error?) {
var err: NSError?
let res = nebula!.getHostInfo(byVpnIp: args["vpnIp"] as? String, pending: args["pending"] as! Bool, error: &err)
return (res, err)
}
private func setRemoteForTunnel(args: Dictionary<String, Any>) -> (String?, Error?) {
var err: NSError?
let res = nebula!.setRemoteForTunnel(args["vpnIp"] as? String, addr: args["addr"] as? String, error: &err)
return (res, err)
}
private func closeTunnel(args: Dictionary<String, Any>) -> (Bool?, Error?) {
let res = nebula!.closeTunnel(args["vpnIp"] as? String)
return (res, nil)
}
}

View File

@ -0,0 +1,383 @@
import NetworkExtension
import MobileNebula
extension String: Error {}
class IPCMessage: NSObject, NSCoding {
var id: String
var type: String
var message: Any?
func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(type, forKey: "type")
aCoder.encode(message, forKey: "message")
}
required init(coder aDecoder: NSCoder) {
id = aDecoder.decodeObject(forKey: "id") as! String
type = aDecoder.decodeObject(forKey: "type") as! String
message = aDecoder.decodeObject(forKey: "message") as Any?
}
init(id: String, type: String, message: Any) {
self.id = id
self.type = type
self.message = message
}
}
class IPCRequest: NSObject, NSCoding {
var type: String
var callbackId: String
var arguments: Dictionary<String, Any>?
func encode(with aCoder: NSCoder) {
aCoder.encode(type, forKey: "type")
aCoder.encode(arguments, forKey: "arguments")
aCoder.encode(callbackId, forKey: "callbackId")
}
required init(coder aDecoder: NSCoder) {
callbackId = aDecoder.decodeObject(forKey: "callbackId") as! String
type = aDecoder.decodeObject(forKey: "type") as! String
arguments = aDecoder.decodeObject(forKey: "arguments") as? Dictionary<String, Any>
}
init(callbackId: String, type: String, arguments: Dictionary<String, Any>?) {
self.callbackId = callbackId
self.type = type
self.arguments = arguments
}
init(callbackId: String, type: String) {
self.callbackId = callbackId
self.type = type
}
}
struct CertificateInfo: Codable {
var cert: Certificate
var rawCert: String
var validity: CertificateValidity
enum CodingKeys: String, CodingKey {
case cert = "Cert"
case rawCert = "RawCert"
case validity = "Validity"
}
}
struct Certificate: Codable {
var fingerprint: String
var signature: String
var details: CertificateDetails
/// An empty initilizer to make error reporting easier
init() {
fingerprint = ""
signature = ""
details = CertificateDetails()
}
}
struct CertificateDetails: Codable {
var name: String
var notBefore: String
var notAfter: String
var publicKey: String
var groups: [String]
var ips: [String]
var subnets: [String]
var isCa: Bool
var issuer: String
/// An empty initilizer to make error reporting easier
init() {
name = ""
notBefore = ""
notAfter = ""
publicKey = ""
groups = []
ips = ["ERROR"]
subnets = []
isCa = false
issuer = ""
}
}
struct CertificateValidity: Codable {
var valid: Bool
var reason: String
enum CodingKeys: String, CodingKey {
case valid = "Valid"
case reason = "Reason"
}
}
let statusMap: Dictionary<NEVPNStatus, Bool> = [
NEVPNStatus.invalid: false,
NEVPNStatus.disconnected: false,
NEVPNStatus.connecting: true,
NEVPNStatus.connected: true,
NEVPNStatus.reasserting: true,
NEVPNStatus.disconnecting: true,
]
let statusString: Dictionary<NEVPNStatus, String> = [
NEVPNStatus.invalid: "Invalid configuration",
NEVPNStatus.disconnected: "Disconnected",
NEVPNStatus.connecting: "Connecting...",
NEVPNStatus.connected: "Connected",
NEVPNStatus.reasserting: "Reasserting...",
NEVPNStatus.disconnecting: "Disconnecting...",
]
// Represents a site that was pulled out of the system configuration
struct Site: Codable {
// Stored in manager
var name: String
var id: String
// Stored in proto
var staticHostmap: Dictionary<String, StaticHosts>
var unsafeRoutes: [UnsafeRoute]
var cert: CertificateInfo?
var ca: [CertificateInfo]
var lhDuration: Int
var port: Int
var mtu: Int
var cipher: String
var sortKey: Int
var logVerbosity: String
var connected: Bool?
var status: String?
var logFile: String?
// A list of error encountered when trying to rehydrate a site from config
var errors: [String]
// We initialize to avoid an error with Codable, there is probably a better way since manager must be present for a Site but is not codable
var manager: NETunnelProviderManager = NETunnelProviderManager()
// Creates a new site from a vpn manager instance
init(manager: NETunnelProviderManager) throws {
//TODO: Throw an error and have Sites delete the site, notify the user instead of using !
let proto = manager.protocolConfiguration as! NETunnelProviderProtocol
try self.init(proto: proto)
self.manager = manager
self.connected = statusMap[manager.connection.status]
self.status = statusString[manager.connection.status]
}
init(proto: NETunnelProviderProtocol) throws {
let dict = proto.providerConfiguration
let rawConfig = dict?["config"] as? Data ?? Data()
let decoder = JSONDecoder()
let incoming = try decoder.decode(IncomingSite.self, from: rawConfig)
self.init(incoming: incoming)
}
init(incoming: IncomingSite) {
var err: NSError?
errors = []
name = incoming.name
id = incoming.id
staticHostmap = incoming.staticHostmap
unsafeRoutes = incoming.unsafeRoutes ?? []
do {
let rawCert = incoming.cert
let rawDetails = MobileNebulaParseCerts(rawCert, &err)
if (err != nil) {
throw err!
}
var certs: [CertificateInfo]
certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!)
if (certs.count == 0) {
throw "No certificate found"
}
cert = certs[0]
if (!cert!.validity.valid) {
errors.append("Certificate is invalid: \(cert!.validity.reason)")
}
} catch {
errors.append("Error while loading certificate: \(error.localizedDescription)")
}
do {
let rawCa = incoming.ca
let rawCaDetails = MobileNebulaParseCerts(rawCa, &err)
if (err != nil) {
throw err!
}
ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!)
var hasErrors = false
ca.forEach { cert in
if (!cert.validity.valid) {
hasErrors = true
}
}
if (hasErrors) {
errors.append("There are issues with 1 or more ca certificates")
}
} catch {
ca = []
errors.append("Error while loading certificate authorities: \(error.localizedDescription)")
}
lhDuration = incoming.lhDuration
port = incoming.port
cipher = incoming.cipher
sortKey = incoming.sortKey ?? 0
logVerbosity = incoming.logVerbosity ?? "info"
mtu = incoming.mtu ?? 1300
logFile = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")?.appendingPathComponent(id).appendingPathExtension("log").path
}
// Gets the private key from the keystore, we don't always need it in memory
func getKey() throws -> String {
guard let keyData = KeyChain.load(key: "\(id).key") else {
throw "failed to get key material from keychain"
}
//TODO: make sure this is valid on return!
return String(decoding: keyData, as: UTF8.self)
}
// Limits what we export to the UI
private enum CodingKeys: String, CodingKey {
case name
case id
case staticHostmap
case cert
case ca
case lhDuration
case port
case cipher
case sortKey
case connected
case status
case logFile
case unsafeRoutes
case logVerbosity
case errors
case mtu
}
}
class StaticHosts: Codable {
var lighthouse: Bool
var destinations: [String]
}
class UnsafeRoute: Codable {
var route: String
var via: String
var mtu: Int?
}
// This class represents a site coming in from flutter, meant only to be saved and re-loaded as a proper Site
struct IncomingSite: Codable {
var name: String
var id: String
var staticHostmap: Dictionary<String, StaticHosts>
var unsafeRoutes: [UnsafeRoute]?
var cert: String
var ca: String
var lhDuration: Int
var port: Int
var mtu: Int?
var cipher: String
var sortKey: Int?
var logVerbosity: String?
var key: String?
func save(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) {
#if targetEnvironment(simulator)
let fileManager = FileManager.default
let sitePath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sites").appendingPathComponent(self.id)
let encoder = JSONEncoder()
do {
var config = self
config.key = nil
let rawConfig = try encoder.encode(config)
try rawConfig.write(to: sitePath)
} catch {
return callback(error)
}
callback(nil)
#else
if (manager != nil) {
// We need to refresh our settings to properly update config
manager?.loadFromPreferences { error in
if (error != nil) {
return callback(error)
}
return self.finish(manager: manager!, callback: callback)
}
return
}
return finish(manager: NETunnelProviderManager(), callback: callback)
#endif
}
private func finish(manager: NETunnelProviderManager, callback: @escaping (Error?) -> ()) {
var config = self
// Store the private key if it was provided
if (config.key != nil) {
//TODO: should we ensure the resulting data is big enough? (conversion didn't fail)
let data = config.key!.data(using: .utf8)
if (!KeyChain.save(key: "\(config.id).key", data: data!)) {
return callback("failed to store key material in keychain")
}
}
// Zero out the key so that we don't save it in the profile
config.key = nil
// Stuff our details in the protocol
let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
let encoder = JSONEncoder()
let rawConfig: Data
// We tried using NSSecureCoder but that was obnoxious and didn't work so back to JSON
do {
rawConfig = try encoder.encode(config)
} catch {
return callback(error)
}
proto.providerConfiguration = ["config": rawConfig]
//TODO: proto is a subclass and we should probably set some settings on the parents
//TODO: set these to meaningful values, or not at all
proto.serverAddress = "TODO"
proto.username = "TEST USERNAME"
// Finish up the manager, this is what stores everything at the system level
manager.protocolConfiguration = proto
//TODO: cert name? manager.protocolConfiguration?.username
//TODO: This is what is shown on the vpn page. We should add more identifying details in
manager.localizedDescription = config.name
manager.isEnabled = true
manager.saveToPreferences{ error in
return callback(error)
}
}
}

91
ios/Podfile Normal file
View File

@ -0,0 +1,91 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
return [];
end
generated_key_values = {}
skip_line_start_symbols = ["#", "/"]
File.foreach(file_abs_path) do |line|
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern=separator)
if plugin.length == 2
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
generated_key_values[podname] = podpath
else
puts "Invalid plugin specification: #{line}"
end
end
generated_key_values
end
target 'Runner' do
use_frameworks!
use_modular_headers!
# Flutter Pod
copied_flutter_dir = File.join(__dir__, 'Flutter')
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
unless File.exist?(generated_xcode_build_settings_path)
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
unless File.exist?(copied_framework_path)
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
end
unless File.exist?(copied_podspec_path)
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
end
end
# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
pod 'MMWormhole', '~> 2.0.0'
# Plugin Pods
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
# referring to absolute paths on developers' machines.
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
plugin_pods = parse_KV_file('../.flutter-plugins')
plugin_pods.each do |name, path|
symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
pod name, :path => File.join(symlink, 'ios')
end
end
# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
install! 'cocoapods', :disable_input_output_paths => true
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end

148
ios/Podfile.lock Normal file
View File

@ -0,0 +1,148 @@
PODS:
- barcode_scan (0.0.1):
- Flutter
- MTBBarcodeScanner
- SwiftProtobuf
- DKImagePickerController/Core (4.3.0):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.0)
- DKImagePickerController/PhotoGallery (4.3.0):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.0)
- DKPhotoGallery (0.0.15):
- DKPhotoGallery/Core (= 0.0.15)
- DKPhotoGallery/Model (= 0.0.15)
- DKPhotoGallery/Preview (= 0.0.15)
- DKPhotoGallery/Resource (= 0.0.15)
- SDWebImage
- SDWebImageFLPlugin
- DKPhotoGallery/Core (0.0.15):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SDWebImageFLPlugin
- DKPhotoGallery/Model (0.0.15):
- SDWebImage
- SDWebImageFLPlugin
- DKPhotoGallery/Preview (0.0.15):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SDWebImageFLPlugin
- DKPhotoGallery/Resource (0.0.15):
- SDWebImage
- SDWebImageFLPlugin
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- FLAnimatedImage (1.0.12)
- Flutter (1.0.0)
- flutter_plugin_android_lifecycle (0.0.1):
- Flutter
- flutter_share (0.0.1):
- Flutter
- MMWormhole (2.0.0):
- MMWormhole/Core (= 2.0.0)
- MMWormhole/Core (2.0.0)
- MTBBarcodeScanner (5.0.11)
- package_info (0.0.1):
- Flutter
- path_provider (0.0.1):
- Flutter
- path_provider_linux (0.0.1):
- Flutter
- path_provider_macos (0.0.1):
- Flutter
- SDWebImage (5.8.0):
- SDWebImage/Core (= 5.8.0)
- SDWebImage/Core (5.8.0)
- SDWebImageFLPlugin (0.4.0):
- FLAnimatedImage (>= 1.0.11)
- SDWebImage/Core (~> 5.6)
- SwiftProtobuf (1.8.0)
- url_launcher (0.0.1):
- Flutter
- url_launcher_macos (0.0.1):
- Flutter
- url_launcher_web (0.0.1):
- Flutter
DEPENDENCIES:
- barcode_scan (from `.symlinks/plugins/barcode_scan/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`)
- flutter_share (from `.symlinks/plugins/flutter_share/ios`)
- MMWormhole (~> 2.0.0)
- package_info (from `.symlinks/plugins/package_info/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- path_provider_linux (from `.symlinks/plugins/path_provider_linux/ios`)
- path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
- url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`)
- url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- FLAnimatedImage
- MMWormhole
- MTBBarcodeScanner
- SDWebImage
- SDWebImageFLPlugin
- SwiftProtobuf
EXTERNAL SOURCES:
barcode_scan:
:path: ".symlinks/plugins/barcode_scan/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_plugin_android_lifecycle:
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
flutter_share:
:path: ".symlinks/plugins/flutter_share/ios"
package_info:
:path: ".symlinks/plugins/package_info/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
path_provider_linux:
:path: ".symlinks/plugins/path_provider_linux/ios"
path_provider_macos:
:path: ".symlinks/plugins/path_provider_macos/ios"
url_launcher:
:path: ".symlinks/plugins/url_launcher/ios"
url_launcher_macos:
:path: ".symlinks/plugins/url_launcher_macos/ios"
url_launcher_web:
:path: ".symlinks/plugins/url_launcher_web/ios"
SPEC CHECKSUMS:
barcode_scan: a5c27959edfafaa0c771905bad0b29d6d39e4479
DKImagePickerController: 397702a3590d4958fad336e9a77079935c500ddb
DKPhotoGallery: e880aef16c108333240e1e7327896f2ea380f4f0
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35
flutter_share: 4be0208963c60b537e6255ed2ce1faae61cd9ac2
MMWormhole: 0cd3fd35a9118b2e2d762b499f54eeaace0be791
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
path_provider_linux: 4d630dc393e1f20364f3e3b4a2ff41d9674a84e4
path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
SDWebImage: 84000f962cbfa70c07f19d2234cbfcf5d779b5dc
SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8
SwiftProtobuf: 2cbd9409689b7df170d82a92a33443c8e3e14a70
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313
url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c
PODFILE CHECKSUM: 40eeeb97ef2165edffec94ac1ddff2d14dd420f7
COCOAPODS: 1.9.0

View File

@ -0,0 +1,849 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
437F72592469AAC500A0C4B9 /* Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437F72582469AAC500A0C4B9 /* Site.swift */; };
437F725E2469AC5700A0C4B9 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437F725C2469AC5700A0C4B9 /* Keychain.swift */; };
437F725F2469B4B000A0C4B9 /* Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437F72582469AAC500A0C4B9 /* Site.swift */; };
437F72602469B4B300A0C4B9 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437F725C2469AC5700A0C4B9 /* Keychain.swift */; };
43871C9B2444DD39004F9075 /* MobileNebula.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43871C9A2444DD39004F9075 /* MobileNebula.framework */; };
43871C9D2444E2EC004F9075 /* Sites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43871C9C2444E2EC004F9075 /* Sites.swift */; };
43871C9E2444E61F004F9075 /* MobileNebula.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43871C9A2444DD39004F9075 /* MobileNebula.framework */; };
43AA894F2444D8BC00EDC39C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43AA894E2444D8BC00EDC39C /* NetworkExtension.framework */; };
43AA89572444DA6500EDC39C /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */; };
43AA895C2444DA6500EDC39C /* NebulaNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 43AA89542444DA6500EDC39C /* NebulaNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
43AA89622444DAA500EDC39C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43AA894E2444D8BC00EDC39C /* NetworkExtension.framework */; };
4CF2F06A02A63B862C9F6F03 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384887B4785D38431E800D3A /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
43AA895A2444DA6500EDC39C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 43AA89532444DA6500EDC39C;
remoteInfo = NebulaNetworkExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
43AA89612444DA6500EDC39C /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
43AA895C2444DA6500EDC39C /* NebulaNetworkExtension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
384887B4785D38431E800D3A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
437F72582469AAC500A0C4B9 /* Site.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Site.swift; sourceTree = "<group>"; };
437F725C2469AC5700A0C4B9 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
43871C9A2444DD39004F9075 /* MobileNebula.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MobileNebula.framework; sourceTree = "<group>"; };
43871C9C2444E2EC004F9075 /* Sites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sites.swift; sourceTree = "<group>"; };
43AA894C2444D8BC00EDC39C /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
43AA894E2444D8BC00EDC39C /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
43AA89542444DA6500EDC39C /* NebulaNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NebulaNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
43AA89582444DA6500EDC39C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
43AA89592444DA6500EDC39C /* NebulaNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NebulaNetworkExtension.entitlements; sourceTree = "<group>"; };
43B66ECA245A0C8400B18C36 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
43B66ECC245A146300B18C36 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
43B828DA249C08DC00CA229C /* MMWormhole.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MMWormhole.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6E7A71D8C71BF965D042667D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8E4961BE2F06B97C8C693530 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C2D5198CF6975BF93E8A6F93 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
43AA89512444DA6500EDC39C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
43AA89622444DAA500EDC39C /* NetworkExtension.framework in Frameworks */,
43871C9B2444DD39004F9075 /* MobileNebula.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
43AA894F2444D8BC00EDC39C /* NetworkExtension.framework in Frameworks */,
4CF2F06A02A63B862C9F6F03 /* Pods_Runner.framework in Frameworks */,
43871C9E2444E61F004F9075 /* MobileNebula.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
43AA894D2444D8BC00EDC39C /* Frameworks */ = {
isa = PBXGroup;
children = (
43B828DA249C08DC00CA229C /* MMWormhole.framework */,
43B66ECC245A146300B18C36 /* Foundation.framework */,
43B66ECA245A0C8400B18C36 /* CoreFoundation.framework */,
43AA894E2444D8BC00EDC39C /* NetworkExtension.framework */,
384887B4785D38431E800D3A /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
43AA89552444DA6500EDC39C /* NebulaNetworkExtension */ = {
isa = PBXGroup;
children = (
437F725C2469AC5700A0C4B9 /* Keychain.swift */,
43871C9A2444DD39004F9075 /* MobileNebula.framework */,
43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */,
43AA89582444DA6500EDC39C /* Info.plist */,
43AA89592444DA6500EDC39C /* NebulaNetworkExtension.entitlements */,
437F72582469AAC500A0C4B9 /* Site.swift */,
);
path = NebulaNetworkExtension;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
43AA89552444DA6500EDC39C /* NebulaNetworkExtension */,
97C146EF1CF9000F007C117D /* Products */,
43AA894D2444D8BC00EDC39C /* Frameworks */,
9D19B3FACD187D51D2854929 /* Pods */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
43AA89542444DA6500EDC39C /* NebulaNetworkExtension.appex */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
43AA894C2444D8BC00EDC39C /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
43871C9C2444E2EC004F9075 /* Sites.swift */,
);
path = Runner;
sourceTree = "<group>";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
isa = PBXGroup;
children = (
);
name = "Supporting Files";
sourceTree = "<group>";
};
9D19B3FACD187D51D2854929 /* Pods */ = {
isa = PBXGroup;
children = (
C2D5198CF6975BF93E8A6F93 /* Pods-Runner.debug.xcconfig */,
6E7A71D8C71BF965D042667D /* Pods-Runner.release.xcconfig */,
8E4961BE2F06B97C8C693530 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
43AA89532444DA6500EDC39C /* NebulaNetworkExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 43AA895D2444DA6500EDC39C /* Build configuration list for PBXNativeTarget "NebulaNetworkExtension" */;
buildPhases = (
43AA89632444DAD100EDC39C /* ShellScript */,
43AA89502444DA6500EDC39C /* Sources */,
43AA89512444DA6500EDC39C /* Frameworks */,
43AA89522444DA6500EDC39C /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = NebulaNetworkExtension;
productName = NebulaNetworkExtension;
productReference = 43AA89542444DA6500EDC39C /* NebulaNetworkExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
FF0E0EB9A684F086443A8FBA /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
43AA89612444DA6500EDC39C /* Embed App Extensions */,
00C7A79AE88792090BDAC68B /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
43AA895B2444DA6500EDC39C /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
43AA89532444DA6500EDC39C = {
CreatedOnToolsVersion = 11.4;
DevelopmentTeam = 576H3XS7FP;
ProvisioningStyle = Automatic;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
DevelopmentTeam = 576H3XS7FP;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
43AA89532444DA6500EDC39C /* NebulaNetworkExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
43AA89522444DA6500EDC39C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
00C7A79AE88792090BDAC68B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n";
};
43AA89632444DAD100EDC39C /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "cd ..\n./gen-artifacts.sh ios\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
FF0E0EB9A684F086443A8FBA /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
43AA89502444DA6500EDC39C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
43AA89572444DA6500EDC39C /* PacketTunnelProvider.swift in Sources */,
437F72592469AAC500A0C4B9 /* Site.swift in Sources */,
437F725E2469AC5700A0C4B9 /* Keychain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
43871C9D2444E2EC004F9075 /* Sites.swift in Sources */,
437F725F2469B4B000A0C4B9 /* Site.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
437F72602469B4B300A0C4B9 /* Keychain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
43AA895B2444DA6500EDC39C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 43AA89532444DA6500EDC39C /* NebulaNetworkExtension */;
targetProxy = 43AA895A2444DA6500EDC39C /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 576H3XS7FP;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
"$(PROJECT_DIR)/NebulaNetworkExtension",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 0.0.30;
PRODUCT_BUNDLE_IDENTIFIER = net.defined.mobileNebula;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
43AA895E2444DA6500EDC39C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = NebulaNetworkExtension/NebulaNetworkExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 576H3XS7FP;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/NebulaNetworkExtension",
);
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 0.0.30;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = net.defined.mobileNebula.NebulaNetworkExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
43AA895F2444DA6500EDC39C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = NebulaNetworkExtension/NebulaNetworkExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 576H3XS7FP;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/NebulaNetworkExtension",
);
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 0.0.30;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = net.defined.mobileNebula.NebulaNetworkExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
43AA89602444DA6500EDC39C /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = NebulaNetworkExtension/NebulaNetworkExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 576H3XS7FP;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/NebulaNetworkExtension",
);
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 0.0.30;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = net.defined.mobileNebula.NebulaNetworkExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 576H3XS7FP;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
"$(PROJECT_DIR)/NebulaNetworkExtension",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 0.0.30;
PRODUCT_BUNDLE_IDENTIFIER = net.defined.mobileNebula;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 576H3XS7FP;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
"$(PROJECT_DIR)/NebulaNetworkExtension",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 0.0.30;
PRODUCT_BUNDLE_IDENTIFIER = net.defined.mobileNebula;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
43AA895D2444DA6500EDC39C /* Build configuration list for PBXNativeTarget "NebulaNetworkExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
43AA895E2444DA6500EDC39C /* Debug */,
43AA895F2444DA6500EDC39C /* Release */,
43AA89602444DA6500EDC39C /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,316 @@
import UIKit
import Flutter
import MobileNebula
import NetworkExtension
import MMWormhole
enum ChannelName {
static let vpn = "net.defined.mobileNebula/NebulaVpnService"
}
func MissingArgumentError(message: String, details: Any?) -> FlutterError {
return FlutterError(code: "missing_argument", message: message, details: details)
}
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private var sites: Sites?
private var wormhole = MMWormhole(applicationGroupIdentifier: "group.net.defined.mobileNebula", optionalDirectory: "ipc")
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
sites = Sites(messenger: controller.binaryMessenger)
let channel = FlutterMethodChannel(name: ChannelName.vpn, binaryMessenger: controller.binaryMessenger)
NSKeyedUnarchiver.setClass(IPCMessage.classForKeyedUnarchiver(), forClassName: "NebulaNetworkExtension.IPCMessage")
wormhole.listenForMessage(withIdentifier: "nebula", listener: self.wormholeListener)
channel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result)
case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result)
case "nebula.renderConfig": return self.nebulaRenderConfig(call: call, result: result)
case "listSites": return self.listSites(result: result)
case "deleteSite": return self.deleteSite(call: call, result: result)
case "saveSite": return self.saveSite(call: call, result: result)
case "startSite": return self.startSite(call: call, result: result)
case "stopSite": return self.stopSite(call: call, result: result)
case "active.listHostmap": self.activeListHostmap(call: call, result: result)
case "active.listPendingHostmap": self.activeListPendingHostmap(call: call, result: result)
case "active.getHostInfo": self.activeGetHostInfo(call: call, result: result)
case "active.setRemoteForTunnel": self.activeSetRemoteForTunnel(call: call, result: result)
case "active.closeTunnel": self.activeCloseTunnel(call: call, result: result)
default:
result(FlutterMethodNotImplemented)
}
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let certs = args["certs"] else { return result(MissingArgumentError(message: "certs is a required argument")) }
var err: NSError?
let json = MobileNebulaParseCerts(certs, &err)
if (err != nil) {
return result(CallFailedError(message: "Error while parsing certificate(s)", details: err!.localizedDescription))
}
return result(json)
}
func nebulaGenerateKeyPair(result: FlutterResult) {
var err: NSError?
let kp = MobileNebulaGenerateKeyPair(&err)
if (err != nil) {
return result(CallFailedError(message: "Error while generating key pairs", details: err!.localizedDescription))
}
return result(kp)
}
func nebulaRenderConfig(call: FlutterMethodCall, result: FlutterResult) {
guard let config = call.arguments as? String else { return result(NoArgumentsError()) }
var err: NSError?
print(config)
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
if (err != nil) {
return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription))
}
return result(yaml)
}
func listSites(result: @escaping FlutterResult) {
self.sites?.loadSites { (sites, err) -> () in
if (err != nil) {
return result(CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
}
let encoder = JSONEncoder()
let data = try! encoder.encode(sites)
let ret = String(data: data, encoding: .utf8)
result(ret)
}
}
func deleteSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let id = call.arguments as? String else { return result(NoArgumentsError()) }
//TODO: stop the site if its running currently
self.sites?.deleteSite(id: id) { error in
if (error != nil) {
result(CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
}
result(nil)
}
}
func saveSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let json = call.arguments as? String else { return result(NoArgumentsError()) }
guard let data = json.data(using: .utf8) else { return result(NoArgumentsError()) }
guard let site = try? JSONDecoder().decode(IncomingSite.self, from: data) else {
return result(NoArgumentsError())
}
let oldSite = self.sites?.getSite(id: site.id)
site.save(manager: oldSite?.manager) { error in
if (error != nil) {
return result(CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
}
result(nil)
}
}
func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
#if targetEnvironment(simulator)
let updater = self.sites?.getUpdater(id: id)
updater?.update(connected: true)
#else
let manager = self.sites?.getSite(id: id)?.manager
manager?.loadFromPreferences{ error in
//TODO: Handle load error
// This is silly but we need to enable the site each time to avoid situations where folks have multiple sites
manager?.isEnabled = true
manager?.saveToPreferences{ error in
//TODO: Handle load error
manager?.loadFromPreferences{ error in
//TODO: Handle load error
do {
try manager?.connection.startVPNTunnel()
} catch {
return result(CallFailedError(message: "Could not start site", details: error.localizedDescription))
}
return result(nil)
}
}
}
#endif
}
func stopSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
#if targetEnvironment(simulator)
let updater = self.sites?.getUpdater(id: id)
updater?.update(connected: false)
#else
let manager = self.sites?.getSite(id: id)?.manager
manager?.loadFromPreferences{ error in
//TODO: Handle load error
manager?.connection.stopVPNTunnel()
return result(nil)
}
#endif
}
func activeListHostmap(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
//TODO: match id for safety?
wormholeRequestWithCallback(type: "listHostmap", arguments: nil) { (data, err) -> () in
if (err != nil) {
return result(CallFailedError(message: err!.localizedDescription))
}
result(data)
}
}
func activeListPendingHostmap(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
//TODO: match id for safety?
wormholeRequestWithCallback(type: "listPendingHostmap", arguments: nil) { (data, err) -> () in
if (err != nil) {
return result(CallFailedError(message: err!.localizedDescription))
}
result(data)
}
}
func activeGetHostInfo(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, Any> else { return result(NoArgumentsError()) }
guard let id = args["id"] as? String else { return result(MissingArgumentError(message: "id is a required argument")) }
guard let vpnIp = args["vpnIp"] as? String else { return result(MissingArgumentError(message: "vpnIp is a required argument")) }
let pending = args["pending"] as? Bool ?? false
//TODO: match id for safety?
wormholeRequestWithCallback(type: "getHostInfo", arguments: ["vpnIp": vpnIp, "pending": pending]) { (data, err) -> () in
if (err != nil) {
return result(CallFailedError(message: err!.localizedDescription))
}
result(data)
}
}
func activeSetRemoteForTunnel(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
guard let vpnIp = args["vpnIp"] else { return result(MissingArgumentError(message: "vpnIp is a required argument")) }
guard let addr = args["addr"] else { return result(MissingArgumentError(message: "addr is a required argument")) }
//TODO: match id for safety?
wormholeRequestWithCallback(type: "setRemoteForTunnel", arguments: ["vpnIp": vpnIp, "addr": addr]) { (data, err) -> () in
if (err != nil) {
return result(CallFailedError(message: err!.localizedDescription))
}
result(data)
}
}
func activeCloseTunnel(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
guard let vpnIp = args["vpnIp"] else { return result(MissingArgumentError(message: "vpnIp is a required argument")) }
//TODO: match id for safety?
wormholeRequestWithCallback(type: "closeTunnel", arguments: ["vpnIp": vpnIp]) { (data, err) -> () in
if (err != nil) {
return result(CallFailedError(message: err!.localizedDescription))
}
result(data as? Bool ?? false)
}
}
func wormholeListener(msg: Any?) {
guard let call = msg as? IPCMessage else {
print("Failed to decode IPCMessage from network extension")
return
}
switch call.type {
case "error":
guard let updater = self.sites?.getUpdater(id: call.id) else {
return print("Could not find site to deliver error to \(call.id): \(String(describing: call.message))")
}
updater.setError(err: call.message as! String)
default:
print("Unknown IPC message type \(call.type)")
}
}
func wormholeRequestWithCallback(type: String, arguments: Dictionary<String, Any>?, completion: @escaping (Any?, Error?) -> ()) {
let uuid = UUID().uuidString
wormhole.listenForMessage(withIdentifier: uuid) { msg -> () in
self.wormhole.stopListeningForMessage(withIdentifier: uuid)
guard let call = msg as? IPCMessage else {
completion("", "Failed to decode IPCMessage callback from network extension")
return
}
switch call.type {
case "error":
completion("", call.message as? String ?? "Failed to convert error")
case "success":
completion(call.message, nil)
default:
completion("", "Unknown IPC message type \(call.type)")
}
}
wormhole.passMessageObject(IPCRequest(callbackId: uuid, type: type, arguments: arguments), identifier: "app")
}
}
func MissingArgumentError(message: String, details: Error? = nil) -> FlutterError {
return FlutterError(code: "missingArgument", message: message, details: details)
}
func NoArgumentsError(message: String? = "no arguments were provided or could not be deserialized", details: Error? = nil) -> FlutterError {
return FlutterError(code: "noArguments", message: message, details: details)
}
func CallFailedError(message: String, details: String? = "") -> FlutterError {
return FlutterError(code: "callFailed", message: message, details: details)
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

51
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>nebula</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Camera permission is required for qr code scanning.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Just in case you store a nebula certificate as a photo, we aren't interested in your photos but a file picker might come across them</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.net.defined.mobileNebula</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)group.net.defined.mobileNebula</string>
</array>
</dict>
</plist>

163
ios/Runner/Sites.swift Normal file
View File

@ -0,0 +1,163 @@
import NetworkExtension
import MobileNebula
class SiteContainer {
var site: Site
var updater: SiteUpdater
init(site: Site, updater: SiteUpdater) {
self.site = site
self.updater = updater
}
}
class Sites {
private var sites = [String: SiteContainer]()
private var messenger: FlutterBinaryMessenger?
init(messenger: FlutterBinaryMessenger?) {
self.messenger = messenger
}
func loadSites(completion: @escaping ([String: Site]?, Error?) -> ()) {
#if targetEnvironment(simulator)
let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sites")
var configPaths: [URL]
do {
if (!fileManager.fileExists(atPath: documentsURL.absoluteString)) {
try fileManager.createDirectory(at: documentsURL, withIntermediateDirectories: true)
}
configPaths = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil)
} catch {
return completion(nil, error)
}
configPaths.forEach { path in
do {
let config = try Data(contentsOf: path)
let decoder = JSONDecoder()
let incoming = try decoder.decode(IncomingSite.self, from: config)
let site = try Site(incoming: incoming)
let updater = SiteUpdater(messenger: self.messenger!, site: site)
self.sites[site.id] = SiteContainer(site: site, updater: updater)
} catch {
print(error)
// try? fileManager.removeItem(at: path)
print("Deleted non conforming site \(path)")
}
}
let justSites = self.sites.mapValues {
return $0.site
}
completion(justSites, nil)
#else
NETunnelProviderManager.loadAllFromPreferences() { newManagers, err in
if (err != nil) {
return completion(nil, err)
}
newManagers?.forEach { manager in
do {
let site = try Site(manager: manager)
// Load the private key to make sure we can
_ = try site.getKey()
let updater = SiteUpdater(messenger: self.messenger!, site: site)
self.sites[site.id] = SiteContainer(site: site, updater: updater)
} catch {
//TODO: notify the user about this
print("Deleted non conforming site \(manager) \(error)")
manager.removeFromPreferences()
}
}
let justSites = self.sites.mapValues {
return $0.site
}
completion(justSites, nil)
}
#endif
}
func deleteSite(id: String, callback: @escaping (Error?) -> ()) {
if let site = self.sites.removeValue(forKey: id) {
#if targetEnvironment(simulator)
let fileManager = FileManager.default
let sitePath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sites").appendingPathComponent(site.site.id)
try? fileManager.removeItem(at: sitePath)
#else
_ = KeyChain.delete(key: site.site.id)
site.site.manager.removeFromPreferences(completionHandler: callback)
#endif
}
// Nothing to remove
callback(nil)
}
func getSite(id: String) -> Site? {
return self.sites[id]?.site
}
func getUpdater(id: String) -> SiteUpdater? {
return self.sites[id]?.updater
}
}
class SiteUpdater: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?;
private var eventChannel: FlutterEventChannel;
private var site: Site
private var notification: Any?
init(messenger: FlutterBinaryMessenger, site: Site) {
eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
self.site = site
super.init()
eventChannel.setStreamHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
eventSink = events;
self.notification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager.connection , queue: nil) { _ in
self.site.status = statusString[self.site.manager.connection.status]
self.site.connected = statusMap[self.site.manager.connection.status]
let d: Dictionary<String, Any> = [
"connected": self.site.connected!,
"status": self.site.status!,
]
self.eventSink?(d)
}
return nil
}
func setError(err: String) {
let d: Dictionary<String, Any> = [
"connected": self.site.connected!,
"status": self.site.status!,
]
self.eventSink?(FlutterError(code: "", message: err, details: d))
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
if (self.notification != nil) {
NotificationCenter.default.removeObserver(self.notification!)
}
return nil
}
func update(connected: Bool) {
let d: Dictionary<String, Any> = [
"connected": connected,
"status": connected ? "Connected" : "Disconnected",
]
self.eventSink?(d)
}
}

View File

@ -0,0 +1,102 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/components/SpecialTextField.dart';
import 'package:mobile_nebula/models/CIDR.dart';
import '../services/utils.dart';
import 'IPField.dart';
//TODO: Support initialValue
class CIDRField extends StatefulWidget {
const CIDRField({
Key key,
this.ipHelp = "ip address",
this.autoFocus = false,
this.focusNode,
this.nextFocusNode,
this.onChanged,
this.textInputAction,
this.ipController,
this.bitsController,
}) : super(key: key);
final String ipHelp;
final bool autoFocus;
final FocusNode focusNode;
final FocusNode nextFocusNode;
final ValueChanged<CIDR> onChanged;
final TextInputAction textInputAction;
final TextEditingController ipController;
final TextEditingController bitsController;
@override
_CIDRFieldState createState() => _CIDRFieldState();
}
//TODO: if the keyboard is open on the port field and you switch to dark mode, it crashes
//TODO: maybe add in a next/done step for numeric keyboards
//TODO: rig up focus node and next node
//TODO: rig up textInputAction
class _CIDRFieldState extends State<CIDRField> {
final bitsFocus = FocusNode();
final cidr = CIDR();
@override
void initState() {
//TODO: this won't track external controller changes appropriately
cidr.ip = widget.ipController?.text ?? "";
cidr.bits = int.tryParse(widget.bitsController?.text ?? "");
super.initState();
}
@override
Widget build(BuildContext context) {
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
return Container(
child: Row(children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(6, 6, 2, 6),
child: IPField(
help: widget.ipHelp,
ipOnly: true,
textPadding: EdgeInsets.all(0),
textInputAction: TextInputAction.next,
textAlign: TextAlign.end,
focusNode: widget.focusNode,
nextFocusNode: bitsFocus,
onChanged: (val) {
cidr.ip = val;
widget.onChanged(cidr);
},
controller: widget.ipController,
))),
Text("/"),
Container(
width: Utils.textSize("bits", textStyle).width + 12,
padding: EdgeInsets.fromLTRB(2, 6, 6, 6),
child: SpecialTextField(
keyboardType: TextInputType.number,
focusNode: bitsFocus,
nextFocusNode: widget.nextFocusNode,
controller: widget.bitsController,
onChanged: (val) {
cidr.bits = int.tryParse(val ?? "");
widget.onChanged(cidr);
},
maxLength: 2,
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
textInputAction: widget.textInputAction ?? TextInputAction.done,
placeholder: 'bits',
))
]));
}
@override
void dispose() {
bitsFocus.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,170 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/components/CIDRField.dart';
import 'package:mobile_nebula/models/CIDR.dart';
import 'package:mobile_nebula/validators/ipValidator.dart';
class CIDRFormField extends FormField<CIDR> {
//TODO: onSaved, validator, autovalidate, enabled?
CIDRFormField({
Key key,
autoFocus = false,
focusNode,
nextFocusNode,
ValueChanged<CIDR> onChanged,
FormFieldSetter<CIDR> onSaved,
textInputAction,
CIDR initialValue,
this.ipController,
this.bitsController,
}) : super(
key: key,
initialValue: initialValue,
onSaved: onSaved,
validator: (cidr) {
if (cidr == null) {
return "Please fill out this field";
}
if (!ipValidator(cidr.ip)) {
return 'Please enter a valid ip address';
}
if (cidr.bits == null || cidr.bits > 32 || cidr.bits < 0) {
return "Please enter a valid number of bits";
}
return null;
},
builder: (FormFieldState<CIDR> field) {
final _CIDRFormField state = field;
void onChangedHandler(CIDR value) {
if (onChanged != null) {
onChanged(value);
}
field.didChange(value);
}
return Column(crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[
CIDRField(
autoFocus: autoFocus,
focusNode: focusNode,
nextFocusNode: nextFocusNode,
onChanged: onChangedHandler,
textInputAction: textInputAction,
ipController: state._effectiveIPController,
bitsController: state._effectiveBitsController,
),
field.hasError
? Text(field.errorText,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
textAlign: TextAlign.end)
: Container(height: 0)
]);
});
final TextEditingController ipController;
final TextEditingController bitsController;
@override
_CIDRFormField createState() => _CIDRFormField();
}
class _CIDRFormField extends FormFieldState<CIDR> {
TextEditingController _ipController;
TextEditingController _bitsController;
TextEditingController get _effectiveIPController => widget.ipController ?? _ipController;
TextEditingController get _effectiveBitsController => widget.bitsController ?? _bitsController;
@override
CIDRFormField get widget => super.widget;
@override
void initState() {
super.initState();
if (widget.ipController == null) {
_ipController = TextEditingController(text: widget.initialValue.ip);
} else {
widget.ipController.addListener(_handleControllerChanged);
}
if (widget.bitsController == null) {
_bitsController = TextEditingController(text: widget.initialValue?.bits?.toString() ?? "");
} else {
widget.bitsController.addListener(_handleControllerChanged);
}
}
@override
void didUpdateWidget(CIDRFormField oldWidget) {
super.didUpdateWidget(oldWidget);
var update = CIDR(ip: widget.ipController?.text, bits: int.tryParse(widget.bitsController?.text ?? "") ?? null);
bool shouldUpdate = false;
if (widget.ipController != oldWidget.ipController) {
oldWidget.ipController?.removeListener(_handleControllerChanged);
widget.ipController?.addListener(_handleControllerChanged);
if (oldWidget.ipController != null && widget.ipController == null) {
_ipController = TextEditingController.fromValue(oldWidget.ipController.value);
}
if (widget.ipController != null) {
shouldUpdate = true;
update.ip = widget.ipController.text;
if (oldWidget.ipController == null) _ipController = null;
}
}
if (widget.bitsController != oldWidget.bitsController) {
oldWidget.bitsController?.removeListener(_handleControllerChanged);
widget.bitsController?.addListener(_handleControllerChanged);
if (oldWidget.bitsController != null && widget.bitsController == null) {
_bitsController = TextEditingController.fromValue(oldWidget.bitsController.value);
}
if (widget.bitsController != null) {
shouldUpdate = true;
update.bits = int.parse(widget.bitsController.text);
if (oldWidget.bitsController == null) _bitsController = null;
}
}
if (shouldUpdate) {
setValue(update);
}
}
@override
void dispose() {
widget.ipController?.removeListener(_handleControllerChanged);
widget.bitsController?.removeListener(_handleControllerChanged);
super.dispose();
}
@override
void reset() {
super.reset();
setState(() {
_effectiveIPController.text = widget.initialValue.ip;
_effectiveBitsController.text = widget.initialValue.bits.toString();
});
}
void _handleControllerChanged() {
// Suppress changes that originated from within this class.
//
// In the case where a controller has been passed in to this widget, we
// register this change listener. In these cases, we'll also receive change
// notifications for changes originating from within this class -- for
// example, the reset() method. In such cases, the FormField value will
// already have been set.
final effectiveBits = int.parse(_effectiveBitsController.text);
if (_effectiveIPController.text != value.ip || effectiveBits != value.bits) {
didChange(CIDR(ip: _effectiveIPController.text, bits: effectiveBits));
}
}
}

View File

@ -0,0 +1,95 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/services/utils.dart';
/// SimplePage with a form and built in validation and confirmation to discard changes if any are made
class FormPage extends StatefulWidget {
const FormPage(
{Key key, this.title, @required this.child, @required this.onSave, @required this.changed, this.hideSave = false})
: super(key: key);
final String title;
final Function onSave;
final Widget child;
/// If you need the page to progress to a certain point before saving, control it here
final bool hideSave;
/// Useful if you have a non form field that can change, overrides the internal changed state if true
final bool changed;
@override
_FormPageState createState() => _FormPageState();
}
class _FormPageState extends State<FormPage> {
var changed = false;
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
changed = widget.changed || changed;
return WillPopScope(
onWillPop: () {
if (!changed) {
return Future.value(true);
}
var completer = Completer<bool>();
Utils.confirmDelete(context, 'Discard changes?', () {
completer.complete(true);
}, deleteLabel: 'Yes', cancelLabel: 'No');
return completer.future;
},
child: SimplePage(
leadingAction: _buildLeader(context),
trailingActions: _buildTrailer(context),
title: widget.title,
child: Form(
key: _formKey,
onChanged: () => setState(() {
changed = true;
}),
child: widget.child),
));
}
Widget _buildLeader(BuildContext context) {
return Utils.leadingBackWidget(context, label: changed ? 'Cancel' : 'Back', onPressed: () {
if (changed) {
Utils.confirmDelete(context, 'Discard changes?', () {
changed = false;
Navigator.pop(context);
}, deleteLabel: 'Yes', cancelLabel: 'No');
} else {
Navigator.pop(context);
}
});
}
List<Widget> _buildTrailer(BuildContext context) {
if (!changed || widget.hideSave) {
return [];
}
return [
Utils.trailingSaveWidget(
context,
() {
if (!_formKey.currentState.validate()) {
return;
}
_formKey.currentState.save();
widget.onSave();
},
)
];
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/components/SpecialTextField.dart';
import 'package:mobile_nebula/models/IPAndPort.dart';
import '../services/utils.dart';
import 'IPField.dart';
//TODO: Support initialValue
class IPAndPortField extends StatefulWidget {
const IPAndPortField({
Key key,
this.ipOnly = false,
this.ipHelp = "ip address",
this.autoFocus = false,
this.focusNode,
this.nextFocusNode,
this.onChanged,
this.textInputAction,
this.noBorder = false,
this.ipTextAlign,
this.ipController,
this.portController,
}) : super(key: key);
final String ipHelp;
final bool ipOnly;
final bool autoFocus;
final FocusNode focusNode;
final FocusNode nextFocusNode;
final ValueChanged<IPAndPort> onChanged;
final TextInputAction textInputAction;
final bool noBorder;
final TextAlign ipTextAlign;
final TextEditingController ipController;
final TextEditingController portController;
@override
_IPAndPortFieldState createState() => _IPAndPortFieldState();
}
//TODO: if the keyboard is open on the port field and you switch to dark mode, it crashes
//TODO: maybe add in a next/done step for numeric keyboards
//TODO: rig up focus node and next node
//TODO: rig up textInputAction
class _IPAndPortFieldState extends State<IPAndPortField> {
final _portFocus = FocusNode();
final _ipAndPort = IPAndPort();
@override
void initState() {
//TODO: this won't track external controller changes appropriately
_ipAndPort.ip = widget.ipController?.text ?? "";
_ipAndPort.port = int.tryParse(widget.portController?.text ?? "");
super.initState();
}
@override
Widget build(BuildContext context) {
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
return Container(
child: Row(children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(6, 6, 2, 6),
child: IPField(
help: widget.ipHelp,
ipOnly: widget.ipOnly,
nextFocusNode: _portFocus,
textPadding: EdgeInsets.all(0),
textInputAction: TextInputAction.next,
focusNode: widget.focusNode,
onChanged: (val) {
_ipAndPort.ip = val;
widget.onChanged(_ipAndPort);
},
textAlign: widget.ipTextAlign,
controller: widget.ipController,
))),
Text(":"),
Container(
width: Utils.textSize("00000", textStyle).width + 12,
padding: EdgeInsets.fromLTRB(2, 6, 6, 6),
child: SpecialTextField(
keyboardType: TextInputType.number,
focusNode: _portFocus,
nextFocusNode: widget.nextFocusNode,
controller: widget.portController,
onChanged: (val) {
_ipAndPort.port = int.tryParse(val ?? "");
widget.onChanged(_ipAndPort);
},
maxLength: 5,
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
textInputAction: TextInputAction.done,
placeholder: 'port',
))
]));
}
@override
void dispose() {
_portFocus.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,180 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/models/IPAndPort.dart';
import 'package:mobile_nebula/validators/dnsValidator.dart';
import 'package:mobile_nebula/validators/ipValidator.dart';
import 'IPAndPortField.dart';
class IPAndPortFormField extends FormField<IPAndPort> {
//TODO: onSaved, validator, autovalidate, enabled?
IPAndPortFormField({
Key key,
ipOnly = false,
ipHelp = "ip address",
autoFocus = false,
focusNode,
nextFocusNode,
ValueChanged<IPAndPort> onChanged,
FormFieldSetter<IPAndPort> onSaved,
textInputAction,
IPAndPort initialValue,
noBorder,
ipTextAlign = TextAlign.center,
this.ipController,
this.portController,
}) : super(
key: key,
initialValue: initialValue,
onSaved: onSaved,
validator: (ipAndPort) {
if (ipAndPort == null) {
return "Please fill out this field";
}
if (!ipValidator(ipAndPort.ip) && (!ipOnly && !dnsValidator(ipAndPort.ip))) {
return ipOnly ? 'Please enter a valid ip address' : 'Please enter a valid ip address or dns name';
}
if (ipAndPort.port == null || ipAndPort.port > 65535 || ipAndPort.port < 0) {
return "Please enter a valid port";
}
return null;
},
builder: (FormFieldState<IPAndPort> field) {
final _IPAndPortFormField state = field;
void onChangedHandler(IPAndPort value) {
if (onChanged != null) {
onChanged(value);
}
field.didChange(value);
}
return Column(children: <Widget>[
IPAndPortField(
ipOnly: ipOnly,
ipHelp: ipHelp,
autoFocus: autoFocus,
focusNode: focusNode,
nextFocusNode: nextFocusNode,
onChanged: onChangedHandler,
textInputAction: textInputAction,
ipController: state._effectiveIPController,
portController: state._effectivePortController,
noBorder: noBorder,
ipTextAlign: ipTextAlign,
),
field.hasError
? Text(field.errorText,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13))
: Container(height: 0)
]);
});
final TextEditingController ipController;
final TextEditingController portController;
@override
_IPAndPortFormField createState() => _IPAndPortFormField();
}
class _IPAndPortFormField extends FormFieldState<IPAndPort> {
TextEditingController _ipController;
TextEditingController _portController;
TextEditingController get _effectiveIPController => widget.ipController ?? _ipController;
TextEditingController get _effectivePortController => widget.portController ?? _portController;
@override
IPAndPortFormField get widget => super.widget;
@override
void initState() {
super.initState();
if (widget.ipController == null) {
_ipController = TextEditingController(text: widget.initialValue.ip);
} else {
widget.ipController.addListener(_handleControllerChanged);
}
if (widget.portController == null) {
_portController = TextEditingController(text: widget.initialValue?.port?.toString() ?? "");
} else {
widget.portController.addListener(_handleControllerChanged);
}
}
@override
void didUpdateWidget(IPAndPortFormField oldWidget) {
super.didUpdateWidget(oldWidget);
var update =
IPAndPort(ip: widget.ipController?.text, port: int.tryParse(widget.portController?.text ?? "") ?? null);
bool shouldUpdate = false;
if (widget.ipController != oldWidget.ipController) {
oldWidget.ipController?.removeListener(_handleControllerChanged);
widget.ipController?.addListener(_handleControllerChanged);
if (oldWidget.ipController != null && widget.ipController == null) {
_ipController = TextEditingController.fromValue(oldWidget.ipController.value);
}
if (widget.ipController != null) {
shouldUpdate = true;
update.ip = widget.ipController.text;
if (oldWidget.ipController == null) _ipController = null;
}
}
if (widget.portController != oldWidget.portController) {
oldWidget.portController?.removeListener(_handleControllerChanged);
widget.portController?.addListener(_handleControllerChanged);
if (oldWidget.portController != null && widget.portController == null) {
_portController = TextEditingController.fromValue(oldWidget.portController.value);
}
if (widget.portController != null) {
shouldUpdate = true;
update.port = int.parse(widget.portController.text);
if (oldWidget.portController == null) _portController = null;
}
}
if (shouldUpdate) {
setValue(update);
}
}
@override
void dispose() {
widget.ipController?.removeListener(_handleControllerChanged);
widget.portController?.removeListener(_handleControllerChanged);
super.dispose();
}
@override
void reset() {
super.reset();
setState(() {
_effectiveIPController.text = widget.initialValue.ip;
_effectivePortController.text = widget.initialValue.port.toString();
});
}
void _handleControllerChanged() {
// Suppress changes that originated from within this class.
//
// In the case where a controller has been passed in to this widget, we
// register this change listener. In these cases, we'll also receive change
// notifications for changes originating from within this class -- for
// example, the reset() method. In such cases, the FormField value will
// already have been set.
final effectivePort = int.parse(_effectivePortController.text);
if (_effectiveIPController.text != value.ip || effectivePort != value.port) {
didChange(IPAndPort(ip: _effectiveIPController.text, port: effectivePort));
}
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SpecialTextField.dart';
import '../services/utils.dart';
class IPField extends StatelessWidget {
final String help;
final bool ipOnly;
final bool autoFocus;
final FocusNode focusNode;
final FocusNode nextFocusNode;
final ValueChanged<String> onChanged;
final EdgeInsetsGeometry textPadding;
final TextInputAction textInputAction;
final controller;
final textAlign;
const IPField(
{Key key,
this.ipOnly = false,
this.help = "ip address",
this.autoFocus = false,
this.focusNode,
this.nextFocusNode,
this.onChanged,
this.textPadding = const EdgeInsets.all(6.0),
this.textInputAction,
this.controller,
this.textAlign = TextAlign.center})
: super(key: key);
@override
Widget build(BuildContext context) {
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
final double ipWidth = ipOnly ? Utils.textSize("000000000000000", textStyle).width + 12 : null;
return SizedBox(
width: ipWidth,
child: SpecialTextField(
keyboardType: ipOnly ? TextInputType.numberWithOptions(decimal: true) : null,
textAlign: textAlign,
autofocus: autoFocus,
focusNode: focusNode,
nextFocusNode: nextFocusNode,
controller: controller,
onChanged: onChanged,
maxLength: ipOnly ? 15 : null,
maxLengthEnforced: ipOnly ? true : false,
inputFormatters: ipOnly
? [WhitelistingTextInputFormatter(RegExp(r'[\d\.]+'))]
: [WhitelistingTextInputFormatter(RegExp(r'[^\s]+'))],
textInputAction: this.textInputAction,
placeholder: help,
));
}
}

View File

@ -0,0 +1,140 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/validators/dnsValidator.dart';
import 'package:mobile_nebula/validators/ipValidator.dart';
import 'IPField.dart';
//TODO: reset doesn't update the ui but clears the field
class IPFormField extends FormField<String> {
//TODO: validator, autovalidate, enabled?
IPFormField({
Key key,
ipOnly = false,
help = "ip address",
autoFocus = false,
focusNode,
nextFocusNode,
ValueChanged<String> onChanged,
FormFieldSetter<String> onSaved,
textPadding = const EdgeInsets.all(6.0),
textInputAction,
initialValue,
this.controller,
crossAxisAlignment = CrossAxisAlignment.center,
textAlign = TextAlign.center,
}) : super(
key: key,
initialValue: initialValue,
onSaved: onSaved,
validator: (ip) {
if (ip == null || ip == "") {
return "Please fill out this field";
}
if (!ipValidator(ip) || (!ipOnly && !dnsValidator(ip))) {
print(ip);
return ipOnly ? 'Please enter a valid ip address' : 'Please enter a valid ip address or dns name';
}
return null;
},
builder: (FormFieldState<String> field) {
final _IPFormField state = field;
void onChangedHandler(String value) {
if (onChanged != null) {
onChanged(value);
}
field.didChange(value);
}
return Column(crossAxisAlignment: crossAxisAlignment, children: <Widget>[
IPField(
ipOnly: ipOnly,
help: help,
autoFocus: autoFocus,
focusNode: focusNode,
nextFocusNode: nextFocusNode,
onChanged: onChangedHandler,
textPadding: textPadding,
textInputAction: textInputAction,
controller: state._effectiveController,
textAlign: textAlign),
field.hasError
? Text(
field.errorText,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
textAlign: textAlign,
)
: Container(height: 0)
]);
});
final TextEditingController controller;
@override
_IPFormField createState() => _IPFormField();
}
class _IPFormField extends FormFieldState<String> {
TextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller;
@override
IPFormField get widget => super.widget;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = TextEditingController(text: widget.initialValue);
} else {
widget.controller.addListener(_handleControllerChanged);
}
}
@override
void didUpdateWidget(IPFormField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?.removeListener(_handleControllerChanged);
widget.controller?.addListener(_handleControllerChanged);
if (oldWidget.controller != null && widget.controller == null)
_controller = TextEditingController.fromValue(oldWidget.controller.value);
if (widget.controller != null) {
setValue(widget.controller.text);
if (oldWidget.controller == null) _controller = null;
}
}
}
@override
void dispose() {
widget.controller?.removeListener(_handleControllerChanged);
super.dispose();
}
@override
void reset() {
super.reset();
setState(() {
_effectiveController.text = widget.initialValue;
});
}
void _handleControllerChanged() {
// Suppress changes that originated from within this class.
//
// In the case where a controller has been passed in to this widget, we
// register this change listener. In these cases, we'll also receive change
// notifications for changes originating from within this class -- for
// example, the reset() method. In such cases, the FormField value will
// already have been set.
if (_effectiveController.text != value) didChange(_effectiveController.text);
}
}

View File

@ -0,0 +1,150 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/components/SpecialTextField.dart';
//TODO: reset doesn't update the ui but clears the field
class PlatformTextFormField extends FormField<String> {
//TODO: autovalidate, enabled?
PlatformTextFormField(
{Key key,
widgetKey,
this.controller,
focusNode,
nextFocusNode,
TextInputType keyboardType,
textInputAction,
List<TextInputFormatter> inputFormatters,
textAlign,
autofocus,
maxLines = 1,
maxLength,
maxLengthEnforced,
onChanged,
keyboardAppearance,
minLines,
expands,
suffix,
textAlignVertical,
String initialValue,
String placeholder,
FormFieldValidator<String> validator,
ValueChanged<String> onSaved})
: super(
key: key,
initialValue: controller != null ? controller.text : (initialValue ?? ''),
onSaved: onSaved,
validator: (str) {
if (validator != null) {
return validator(str);
}
return null;
},
builder: (FormFieldState<String> field) {
final _PlatformTextFormFieldState state = field;
void onChangedHandler(String value) {
if (onChanged != null) {
onChanged(value);
}
field.didChange(value);
}
return Column(crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[
SpecialTextField(
key: widgetKey,
controller: state._effectiveController,
focusNode: focusNode,
nextFocusNode: nextFocusNode,
keyboardType: keyboardType,
textInputAction: textInputAction,
textAlign: textAlign,
autofocus: autofocus,
maxLines: maxLines,
maxLength: maxLength,
maxLengthEnforced: maxLengthEnforced,
onChanged: onChangedHandler,
keyboardAppearance: keyboardAppearance,
minLines: minLines,
expands: expands,
textAlignVertical: textAlignVertical,
placeholder: placeholder,
inputFormatters: inputFormatters,
suffix: suffix),
field.hasError
? Text(
field.errorText,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
textAlign: textAlign,
)
: Container(height: 0)
]);
});
final TextEditingController controller;
@override
_PlatformTextFormFieldState createState() => _PlatformTextFormFieldState();
}
class _PlatformTextFormFieldState extends FormFieldState<String> {
TextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller;
@override
PlatformTextFormField get widget => super.widget;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = TextEditingController(text: widget.initialValue);
} else {
widget.controller.addListener(_handleControllerChanged);
}
}
@override
void didUpdateWidget(PlatformTextFormField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?.removeListener(_handleControllerChanged);
widget.controller?.addListener(_handleControllerChanged);
if (oldWidget.controller != null && widget.controller == null)
_controller = TextEditingController.fromValue(oldWidget.controller.value);
if (widget.controller != null) {
setValue(widget.controller.text);
if (oldWidget.controller == null) _controller = null;
}
}
}
@override
void dispose() {
widget.controller?.removeListener(_handleControllerChanged);
super.dispose();
}
@override
void reset() {
super.reset();
setState(() {
_effectiveController.text = widget.initialValue;
});
}
void _handleControllerChanged() {
// Suppress changes that originated from within this class.
//
// In the case where a controller has been passed in to this widget, we
// register this change listener. In these cases, we'll also receive change
// notifications for changes originating from within this class -- for
// example, the reset() method. In such cases, the FormField value will
// already have been set.
if (_effectiveController.text != value) didChange(_effectiveController.text);
}
}

View File

@ -0,0 +1,105 @@
import 'package:flutter/cupertino.dart' as cupertino;
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/services/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
enum SimpleScrollable {
none,
vertical,
horizontal,
both,
}
class SimplePage extends StatelessWidget {
const SimplePage(
{Key key,
this.title,
@required this.child,
this.leadingAction,
this.trailingActions = const [],
this.scrollable = SimpleScrollable.vertical,
this.scrollbar = true,
this.scrollController,
this.bottomBar,
this.onRefresh,
this.onLoading,
this.refreshController})
: super(key: key);
final String title;
final Widget child;
final SimpleScrollable scrollable;
final ScrollController scrollController;
/// Set this to true to force draw a scrollbar without a scroll view, this is helpful for pages with Reorderable listviews
/// This is set to true if you have any scrollable other than none
final bool scrollbar;
final Widget bottomBar;
/// If no leading action is provided then a default "Back" widget than pops the page will be provided
final Widget leadingAction;
final List<Widget> trailingActions;
final VoidCallback onRefresh;
final VoidCallback onLoading;
final RefreshController refreshController;
@override
Widget build(BuildContext context) {
Widget realChild = child;
var addScrollbar = this.scrollbar;
if (scrollable == SimpleScrollable.vertical || scrollable == SimpleScrollable.both) {
realChild = SingleChildScrollView(scrollDirection: Axis.vertical, child: realChild, controller: refreshController == null ? scrollController : null);
addScrollbar = true;
}
if (scrollable == SimpleScrollable.horizontal || scrollable == SimpleScrollable.both) {
realChild = SingleChildScrollView(scrollDirection: Axis.horizontal, child: realChild);
addScrollbar = true;
}
if (refreshController != null) {
realChild = RefreshConfiguration(
headerTriggerDistance: 100,
footerTriggerDistance: -100,
maxUnderScrollExtent: 100,
child: SmartRefresher(
scrollController: scrollController,
onRefresh: onRefresh,
onLoading: onLoading,
controller: refreshController,
child: realChild,
enablePullUp: onLoading != null,
enablePullDown: onRefresh != null,
footer: ClassicFooter(loadStyle: LoadStyle.ShowWhenLoading),
));
addScrollbar = true;
}
if (addScrollbar) {
realChild = Scrollbar(child: realChild);
}
if (bottomBar != null) {
realChild = Column(children: [
Expanded(child: realChild),
bottomBar,
]);
}
return PlatformScaffold(
backgroundColor: cupertino.CupertinoColors.systemGroupedBackground.resolveFrom(context),
appBar: PlatformAppBar(
title: Text(title),
leading: leadingAction != null ? leadingAction : Utils.leadingBackWidget(context),
trailingActions: trailingActions,
ios: (_) => CupertinoNavigationBarData(
transitionBetweenRoutes: false,
),
),
body: SafeArea(child: realChild));
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobile_nebula/components/SpecialButton.dart';
import 'package:mobile_nebula/models/Site.dart';
import 'package:mobile_nebula/services/utils.dart';
class SiteItem extends StatelessWidget {
const SiteItem({Key key, this.site, this.onPressed}) : super(key: key);
final Site site;
final onPressed;
@override
Widget build(BuildContext context) {
final borderColor = site.errors.length > 0
? CupertinoColors.systemRed.resolveFrom(context)
: site.connected
? CupertinoColors.systemGreen.resolveFrom(context)
: CupertinoColors.systemGrey2.resolveFrom(context);
final border = BorderSide(color: borderColor, width: 10);
return Container(
margin: EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(border: Border(left: border)),
child: _buildContent(context));
}
Widget _buildContent(BuildContext context) {
final border = BorderSide(color: Utils.configSectionBorder(context));
var ip = "Error";
if (site.cert != null) {
ip = site.cert.cert.details.ips[0];
}
return SpecialButton(
decoration:
BoxDecoration(border: Border(top: border, bottom: border), color: Utils.configItemBackground(context)),
onPressed: onPressed,
child: Padding(
padding: EdgeInsets.fromLTRB(10, 10, 5, 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(site.name, style: TextStyle(fontWeight: FontWeight.bold)),
Expanded(child: Text(ip, textAlign: TextAlign.end)),
Padding(padding: EdgeInsets.only(right: 10)),
Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18)
],
)));
}
}

View File

@ -0,0 +1,145 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// This is a button that pushes the bare minimum onto you, it doesn't even respect button themes - unless you tell it to
class SpecialButton extends StatefulWidget {
const SpecialButton({Key key, this.child, this.color, this.onPressed, this.useButtonTheme = false, this.decoration}) : super(key: key);
final Widget child;
final Color color;
final bool useButtonTheme;
final BoxDecoration decoration;
final Function onPressed;
@override
_SpecialButtonState createState() => _SpecialButtonState();
}
class _SpecialButtonState extends State<SpecialButton> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Platform.isAndroid ? _buildAndroid() : _buildGeneric();
}
Widget _buildAndroid() {
var textStyle;
if (widget.useButtonTheme) {
textStyle = Theme.of(context).textTheme.button;
}
return Material(
textStyle: textStyle,
child: Ink(
decoration: widget.decoration,
color: widget.color,
child: InkWell(
child: widget.child,
onTap: widget.onPressed,
)));
}
Widget _buildGeneric() {
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
if (widget.useButtonTheme) {
textStyle = CupertinoTheme.of(context).textTheme.actionTextStyle;
}
return Container(
decoration: widget.decoration,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onTapCancel: _handleTapCancel,
onTap: widget.onPressed,
child: Semantics(
button: true,
child: FadeTransition(
opacity: _opacityAnimation,
child: DefaultTextStyle(style: textStyle, child: Container(child: widget.child, color: widget.color)),
),
),
)
);
}
// Eyeballed values. Feel free to tweak.
static const Duration kFadeOutDuration = Duration(milliseconds: 10);
static const Duration kFadeInDuration = Duration(milliseconds: 100);
final Tween<double> _opacityTween = Tween<double>(begin: 1.0);
AnimationController _animationController;
Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
value: 0.0,
vsync: this,
);
_opacityAnimation = _animationController.drive(CurveTween(curve: Curves.decelerate)).drive(_opacityTween);
_setTween();
}
@override
void didUpdateWidget(SpecialButton old) {
super.didUpdateWidget(old);
_setTween();
}
void _setTween() {
_opacityTween.end = 0.4;
}
@override
void dispose() {
_animationController.dispose();
_animationController = null;
super.dispose();
}
bool _buttonHeldDown = false;
void _handleTapDown(TapDownDetails event) {
if (!_buttonHeldDown) {
_buttonHeldDown = true;
_animate();
}
}
void _handleTapUp(TapUpDetails event) {
if (_buttonHeldDown) {
_buttonHeldDown = false;
_animate();
}
}
void _handleTapCancel() {
if (_buttonHeldDown) {
_buttonHeldDown = false;
_animate();
}
}
void _animate() {
if (_animationController.isAnimating) {
return;
}
final bool wasHeldDown = _buttonHeldDown;
final TickerFuture ticker = _buttonHeldDown
? _animationController.animateTo(1.0, duration: kFadeOutDuration)
: _animationController.animateTo(0.0, duration: kFadeInDuration);
ticker.then<void>((void value) {
if (mounted && wasHeldDown != _buttonHeldDown) {
_animate();
}
});
}
}

View File

@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
/// A normal TextField or CupertinoTextField that watches for copy, paste, cut, or select all keyboard actions
class SpecialTextField extends StatefulWidget {
const SpecialTextField(
{Key key,
this.placeholder,
this.suffix,
this.controller,
this.focusNode,
this.nextFocusNode,
this.autocorrect,
this.minLines,
this.maxLines,
this.maxLength,
this.maxLengthEnforced,
this.style,
this.keyboardType,
this.textInputAction,
this.textCapitalization,
this.textAlign,
this.autofocus,
this.onChanged,
this.expands,
this.keyboardAppearance,
this.textAlignVertical,
this.inputFormatters})
: super(key: key);
final String placeholder;
final TextEditingController controller;
final FocusNode focusNode;
final FocusNode nextFocusNode;
final bool autocorrect;
final int minLines;
final int maxLines;
final int maxLength;
final bool maxLengthEnforced;
final Widget suffix;
final TextStyle style;
final TextInputType keyboardType;
final Brightness keyboardAppearance;
final TextInputAction textInputAction;
final TextCapitalization textCapitalization;
final TextAlign textAlign;
final TextAlignVertical textAlignVertical;
final bool autofocus;
final ValueChanged<String> onChanged;
final List<TextInputFormatter> inputFormatters;
final bool expands;
@override
_SpecialTextFieldState createState() => _SpecialTextFieldState();
}
class _SpecialTextFieldState extends State<SpecialTextField> {
FocusNode _focusNode = FocusNode();
List<TextInputFormatter> formatters;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
void initState() {
formatters = widget.inputFormatters;
if (formatters == null || formatters.length == 0) {
formatters = [WhitelistingTextInputFormatter(RegExp(r'[^\t]'))];
}
super.initState();
}
@override
Widget build(BuildContext context) {
return RawKeyboardListener(
focusNode: _focusNode,
onKey: _onKey,
child: PlatformTextField(
autocorrect: widget.autocorrect,
minLines: widget.minLines,
maxLines: widget.maxLines,
maxLength: widget.maxLength,
maxLengthEnforced: widget.maxLengthEnforced,
keyboardType: widget.keyboardType,
keyboardAppearance: widget.keyboardAppearance,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
textAlign: widget.textAlign,
textAlignVertical: widget.textAlignVertical,
autofocus: widget.autofocus,
focusNode: widget.focusNode,
onChanged: widget.onChanged,
onSubmitted: (_) {
if (widget.nextFocusNode != null) {
FocusScope.of(context).requestFocus(widget.nextFocusNode);
}
},
expands: widget.expands,
inputFormatters: formatters,
android: (_) => MaterialTextFieldData(
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
hintText: widget.placeholder,
counterText: '',
suffix: widget.suffix)),
ios: (_) => CupertinoTextFieldData(
decoration: BoxDecoration(),
padding: EdgeInsets.zero,
placeholder: widget.placeholder,
suffix: widget.suffix),
style: widget.style,
controller: widget.controller));
}
_onKey(RawKeyEvent event) {
// We don't care about key up events
if (event is RawKeyUpEvent) {
return;
}
if (event.logicalKey == LogicalKeyboardKey.tab) {
// Handle tab to the next node
if (widget.nextFocusNode != null) {
FocusScope.of(context).requestFocus(widget.nextFocusNode);
}
return;
}
// Handle special keyboard events with control key
if (event.data.isControlPressed) {
// Handle paste
if (event.logicalKey == LogicalKeyboardKey.keyV) {
Clipboard.getData("text/plain").then((data) {
// Adjust our clipboard entry to confirm with the leftover space if we have maxLength
var text = data.text;
if (widget.maxLength != null && widget.maxLength > 0) {
var leftover = widget.maxLength - widget.controller.text.length;
if (leftover < data.text.length) {
text = text.substring(0, leftover);
}
}
// If maxLength took us to 0 then bail
if (text.length == 0) {
return;
}
var end = widget.controller.selection.end;
var start = widget.controller.selection.start;
// Insert our paste buffer into the selection, which can be 0 selected text (normal caret)
widget.controller.text = widget.controller.selection.textBefore(widget.controller.text) +
text +
widget.controller.selection.textAfter(widget.controller.text);
// Adjust our caret to be at the end of the pasted contents, need to take into account the size of the selection
// We may want runes instead of
end += text.length - (end - start);
widget.controller.selection = TextSelection(baseOffset: end, extentOffset: end);
});
return;
}
// Handle select all
if (event.logicalKey == LogicalKeyboardKey.keyA) {
widget.controller.selection = TextSelection(baseOffset: 0, extentOffset: widget.controller.text.length);
return;
}
// Handle copy
if (event.logicalKey == LogicalKeyboardKey.keyC) {
Clipboard.setData(ClipboardData(text: widget.controller.selection.textInside(widget.controller.text)));
return;
}
// Handle cut
if (event.logicalKey == LogicalKeyboardKey.keyX) {
Clipboard.setData(ClipboardData(text: widget.controller.selection.textInside(widget.controller.text)));
var start = widget.controller.selection.start;
widget.controller.text = widget.controller.selection.textBefore(widget.controller.text) +
widget.controller.selection.textAfter(widget.controller.text);
widget.controller.selection = TextSelection(baseOffset: start, extentOffset: start);
return;
}
}
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SpecialButton.dart';
import 'package:mobile_nebula/services/utils.dart';
// A config item that detects tapping and calls back on a tap
class ConfigButtonItem extends StatelessWidget {
const ConfigButtonItem({Key key, this.content, this.onPressed}) : super(key: key);
final Widget content;
final onPressed;
@override
Widget build(BuildContext context) {
return SpecialButton(
color: Utils.configItemBackground(context),
onPressed: onPressed,
useButtonTheme: true,
child: Container(
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
child: Center(child: content),
));
return Container(
color: Utils.configItemBackground(context),
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
child: PlatformButton(
androidFlat: (_) => MaterialFlatButtonData(
textTheme: ButtonTextTheme.normal, padding: EdgeInsets.zero, shape: RoundedRectangleBorder()),
ios: (_) => CupertinoButtonData(padding: EdgeInsets.zero, borderRadius: BorderRadius.zero),
padding: EdgeInsets.symmetric(vertical: 7),
child: content,
onPressed: () {
if (onPressed != null) {
onPressed();
}
},
));
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobile_nebula/components/SpecialButton.dart';
import 'package:mobile_nebula/services/utils.dart';
class ConfigCheckboxItem extends StatelessWidget {
const ConfigCheckboxItem({Key key, this.label, this.content, this.labelWidth = 100, this.onChanged, this.checked})
: super(key: key);
final Widget label;
final Widget content;
final double labelWidth;
final bool checked;
final Function onChanged;
@override
Widget build(BuildContext context) {
Widget item = Container(
padding: EdgeInsets.only(left: 15),
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
label != null ? Container(width: labelWidth, child: label) : Container(),
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
checked
? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context), size: 34)
: Container()
],
));
if (onChanged != null) {
return SpecialButton(
color: Utils.configItemBackground(context),
child: item,
onPressed: () {
if (onChanged != null) {
onChanged();
}
},
);
} else {
return item;
}
}
}

View File

@ -0,0 +1,30 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
TextStyle basicTextStyle(BuildContext context) =>
Platform.isIOS ? CupertinoTheme.of(context).textTheme.textStyle : Theme.of(context).textTheme.subhead;
const double _headerFontSize = 13.0;
class ConfigHeader extends StatelessWidget {
const ConfigHeader({Key key, this.label, this.color}) : super(key: key);
final String label;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(left: 10.0, top: 30.0, bottom: 5.0, right: 10.0),
child: Text(
label,
style: basicTextStyle(context).copyWith(
color: color ?? CupertinoColors.secondaryLabel.resolveFrom(context),
fontSize: _headerFontSize,
),
),
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobile_nebula/services/utils.dart';
class ConfigItem extends StatelessWidget {
const ConfigItem(
{Key key, this.label, this.content, this.labelWidth = 100, this.crossAxisAlignment = CrossAxisAlignment.center})
: super(key: key);
final Widget label;
final Widget content;
final double labelWidth;
final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) {
return Container(
color: Utils.configItemBackground(context),
padding: EdgeInsets.only(top: 2, bottom: 2, left: 15, right: 10),
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize),
child: Row(
crossAxisAlignment: crossAxisAlignment,
children: <Widget>[
Container(width: labelWidth, child: label),
Expanded(child: content),
],
));
}
}

View File

@ -0,0 +1,58 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobile_nebula/components/SpecialButton.dart';
import 'package:mobile_nebula/services/utils.dart';
class ConfigPageItem extends StatelessWidget {
const ConfigPageItem(
{Key key,
this.label,
this.content,
this.labelWidth = 100,
this.onPressed,
this.crossAxisAlignment = CrossAxisAlignment.center})
: super(key: key);
final Widget label;
final Widget content;
final double labelWidth;
final CrossAxisAlignment crossAxisAlignment;
final onPressed;
@override
Widget build(BuildContext context) {
var theme;
if (Platform.isAndroid) {
final origTheme = Theme.of(context);
theme = origTheme.copyWith(
textTheme:
origTheme.textTheme.copyWith(button: origTheme.textTheme.button.copyWith(fontWeight: FontWeight.normal)));
return Theme(data: theme, child: _buildContent(context));
} else {
final origTheme = CupertinoTheme.of(context);
theme = origTheme.copyWith(primaryColor: CupertinoColors.label.resolveFrom(context));
return CupertinoTheme(data: theme, child: _buildContent(context));
}
}
Widget _buildContent(BuildContext context) {
return SpecialButton(
onPressed: onPressed,
color: Utils.configItemBackground(context),
child: Container(
padding: EdgeInsets.only(left: 15),
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
child: Row(
crossAxisAlignment: crossAxisAlignment,
children: <Widget>[
label != null ? Container(width: labelWidth, child: label) : Container(),
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18)
],
)),
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobile_nebula/services/utils.dart';
import 'ConfigHeader.dart';
class ConfigSection extends StatelessWidget {
const ConfigSection({Key key, this.label, this.children, this.borderColor, this.labelColor}) : super(key: key);
final List<Widget> children;
final String label;
final Color borderColor;
final Color labelColor;
@override
Widget build(BuildContext context) {
final border = BorderSide(color: borderColor ?? Utils.configSectionBorder(context));
List<Widget> _children = [];
final len = children.length;
for (var i = 0; i < len; i++) {
_children.add(children[i]);
if (i < len - 1) {
double pad = 15;
if (children[i + 1].runtimeType.toString() == 'ConfigButtonItem') {
pad = 0;
}
_children.add(Padding(
child: Divider(height: 1, color: Utils.configSectionBorder(context)), padding: EdgeInsets.only(left: pad)));
}
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
label != null ? ConfigHeader(label: label, color: labelColor) : Container(height: 20),
Container(
decoration:
BoxDecoration(border: Border(top: border, bottom: border), color: Utils.configItemBackground(context)),
child: Column(
children: _children,
))
]);
}
}

View File

@ -0,0 +1,25 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobile_nebula/components/SpecialTextField.dart';
class ConfigTextItem extends StatelessWidget {
const ConfigTextItem({Key key, this.placeholder, this.controller}) : super(key: key);
final String placeholder;
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return Padding(
padding: Platform.isAndroid ? EdgeInsets.all(5) : EdgeInsets.zero,
child: SpecialTextField(
autocorrect: false,
minLines: 3,
maxLines: 10,
placeholder: placeholder,
style: TextStyle(fontFamily: 'RobotoMono'),
controller: controller));
}
}

100
lib/main.dart Normal file
View File

@ -0,0 +1,100 @@
import 'package:flutter/cupertino.dart' show CupertinoThemeData, DefaultCupertinoLocalizations;
import 'package:flutter/material.dart'
show BottomSheetThemeData, Colors, DefaultMaterialLocalizations, Theme, ThemeData, ThemeMode;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/screens/MainScreen.dart';
import 'package:mobile_nebula/services/settings.dart';
//TODO: EventChannel might be better than the streamcontroller we are using now
void main() => runApp(Main());
class Main extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) => App();
}
class App extends StatefulWidget {
@override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
final settings = Settings();
Brightness brightness = SchedulerBinding.instance.window.platformBrightness;
@override
void initState() {
//TODO: wait until settings is ready?
settings.onChange().listen((_) {
setState(() {
if (!settings.useSystemColors) {
brightness = settings.darkMode ? Brightness.dark : Brightness.light;
}
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
final ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blueGrey,
primaryColor: Colors.blueGrey[900],
primaryColorBrightness: Brightness.dark,
accentColor: Colors.cyan[600],
accentColorBrightness: Brightness.dark,
fontFamily: 'PublicSans',
//scaffoldBackgroundColor: Colors.grey[100],
scaffoldBackgroundColor: Colors.white,
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: Colors.blueGrey[50],
),
);
final ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.grey,
primaryColor: Colors.grey[900],
primaryColorBrightness: Brightness.dark,
accentColor: Colors.cyan[600],
accentColorBrightness: Brightness.dark,
fontFamily: 'PublicSans',
scaffoldBackgroundColor: Colors.grey[800],
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: Colors.grey[850],
),
);
// This theme is required since icons light/dark mode will look for it
return Theme(
data: brightness == Brightness.light ? lightTheme : darkTheme,
child: PlatformProvider(
//initialPlatform: initialPlatform,
builder: (context) => PlatformApp(
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
DefaultCupertinoLocalizations.delegate,
],
title: 'Nebula',
android: (_) {
return new MaterialAppData(
themeMode: brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark,
);
},
ios: (_) => CupertinoAppData(
theme: CupertinoThemeData(brightness: brightness),
),
home: MainScreen(),
),
),
);
}
}

25
lib/models/CIDR.dart Normal file
View File

@ -0,0 +1,25 @@
class CIDR {
CIDR({this.ip, this.bits});
String ip;
int bits;
@override
String toString() {
return '$ip/$bits';
}
String toJson() {
return toString();
}
CIDR.fromString(String val) {
final parts = val.split('/');
if (parts.length != 2) {
throw 'Invalid CIDR string';
}
ip = parts[0];
bits = int.parse(parts[1]);
}
}

View File

@ -0,0 +1,83 @@
class CertificateInfo {
Certificate cert;
String rawCert;
CertificateValidity validity;
CertificateInfo.debug({this.rawCert = ""})
: this.cert = Certificate.debug(),
this.validity = CertificateValidity.debug();
CertificateInfo.fromJson(Map<String, dynamic> json)
: cert = Certificate.fromJson(json['Cert']),
rawCert = json['RawCert'],
validity = CertificateValidity.fromJson(json['Validity']);
CertificateInfo({this.cert, this.rawCert, this.validity});
static List<CertificateInfo> fromJsonList(List<dynamic> list) {
return list.map((v) => CertificateInfo.fromJson(v));
}
}
class Certificate {
CertificateDetails details;
String fingerprint;
String signature;
Certificate.debug()
: this.details = CertificateDetails.debug(),
this.fingerprint = "DEBUG",
this.signature = "DEBUG";
Certificate.fromJson(Map<String, dynamic> json)
: details = CertificateDetails.fromJson(json['details']),
fingerprint = json['fingerprint'],
signature = json['signature'];
}
class CertificateDetails {
String name;
DateTime notBefore;
DateTime notAfter;
String publicKey;
List<String> groups;
List<String> ips;
List<String> subnets;
bool isCa;
String issuer;
CertificateDetails.debug()
: this.name = "DEBUG",
notBefore = DateTime.now(),
notAfter = DateTime.now(),
publicKey = "",
groups = [],
ips = [],
subnets = [],
isCa = false,
issuer = "DEBUG";
CertificateDetails.fromJson(Map<String, dynamic> json)
: name = json['name'],
notBefore = DateTime.tryParse(json['notBefore']),
notAfter = DateTime.tryParse(json['notAfter']),
publicKey = json['publicKey'],
groups = List<String>.from(json['groups']),
ips = List<String>.from(json['ips']),
subnets = List<String>.from(json['subnets']),
isCa = json['isCa'],
issuer = json['issuer'];
}
class CertificateValidity {
bool valid;
String reason;
CertificateValidity.debug()
: this.valid = true,
this.reason = "";
CertificateValidity.fromJson(Map<String, dynamic> json)
: valid = json['Valid'],
reason = json['Reason'];
}

49
lib/models/HostInfo.dart Normal file
View File

@ -0,0 +1,49 @@
import 'package:mobile_nebula/models/Certificate.dart';
class HostInfo {
String vpnIp;
int localIndex;
int remoteIndex;
List<UDPAddress> remoteAddresses;
int cachedPackets;
Certificate cert;
UDPAddress currentRemote;
int messageCounter;
HostInfo.fromJson(Map<String, dynamic> json) {
vpnIp = json['vpnIp'];
localIndex = json['localIndex'];
remoteIndex = json['remoteIndex'];
cachedPackets = json['cachedPackets'];
if (json['currentRemote'] != null) {
currentRemote = UDPAddress.fromJson(json['currentRemote']);
}
if (json['cert'] != null) {
cert = Certificate.fromJson(json['cert']);
}
List<dynamic> addrs = json['remoteAddrs'];
remoteAddresses = [];
addrs?.forEach((val) {
remoteAddresses.add(UDPAddress.fromJson(val));
});
messageCounter = json['messageCounter'];
}
}
class UDPAddress {
String ip;
int port;
@override
String toString() {
return '$ip:$port';
}
UDPAddress.fromJson(Map<String, dynamic> json)
: ip = json['IP'],
port = json['Port'];
}

9
lib/models/Hostmap.dart Normal file
View File

@ -0,0 +1,9 @@
import 'IPAndPort.dart';
class Hostmap {
String nebulaIp;
List<IPAndPort> destinations;
bool lighthouse;
Hostmap({this.nebulaIp, this.destinations, this.lighthouse});
}

25
lib/models/IPAndPort.dart Normal file
View File

@ -0,0 +1,25 @@
class IPAndPort {
IPAndPort({this.ip, this.port});
String ip;
int port;
@override
String toString() {
return '$ip:$port';
}
String toJson() {
return toString();
}
IPAndPort.fromString(String val) {
final parts = val.split(':');
if (parts.length != 2) {
throw 'Invalid IPAndPort string';
}
ip = parts[0];
port = int.parse(parts[1]);
}
}

305
lib/models/Site.dart Normal file
View File

@ -0,0 +1,305 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:mobile_nebula/models/HostInfo.dart';
import 'package:mobile_nebula/models/UnsafeRoute.dart';
import 'package:uuid/uuid.dart';
import 'Certificate.dart';
import 'StaticHosts.dart';
var uuid = Uuid();
class Site {
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
EventChannel _updates;
/// Signals that something about this site has changed. onError is called with an error string if there was an error
StreamController _change = StreamController.broadcast();
// Identifiers
String name;
String id;
// static_host_map
Map<String, StaticHost> staticHostmap;
List<UnsafeRoute> unsafeRoutes;
// pki fields
List<CertificateInfo> ca;
CertificateInfo cert;
String key;
// lighthouse options
int lhDuration; // in seconds
// listen settings
int port;
int mtu;
String cipher;
int sortKey;
bool connected;
String status;
String logFile;
String logVerbosity;
// A list of errors encountered while loading the site
List<String> errors;
Site(
{this.name,
id,
staticHostmap,
ca,
this.cert,
this.lhDuration = 0,
this.port = 0,
this.cipher = "aes",
this.sortKey,
this.mtu,
this.connected,
this.status,
this.logFile,
this.logVerbosity,
errors,
unsafeRoutes})
: staticHostmap = staticHostmap ?? {},
unsafeRoutes = unsafeRoutes ?? [],
errors = errors ?? [],
ca = ca ?? [],
id = id ?? uuid.v4();
Site.fromJson(Map<String, dynamic> json) {
name = json['name'];
id = json['id'];
Map<String, dynamic> rawHostmap = json['staticHostmap'];
staticHostmap = {};
rawHostmap.forEach((key, val) {
staticHostmap[key] = StaticHost.fromJson(val);
});
List<dynamic> rawUnsafeRoutes = json['unsafeRoutes'];
unsafeRoutes = [];
if (rawUnsafeRoutes != null) {
rawUnsafeRoutes.forEach((val) {
unsafeRoutes.add(UnsafeRoute.fromJson(val));
});
}
List<dynamic> rawCA = json['ca'];
ca = [];
rawCA.forEach((val) {
ca.add(CertificateInfo.fromJson(val));
});
if (json['cert'] != null) {
cert = CertificateInfo.fromJson(json['cert']);
}
lhDuration = json['lhDuration'];
port = json['port'];
mtu = json['mtu'];
cipher = json['cipher'];
sortKey = json['sortKey'];
logFile = json['logFile'];
logVerbosity = json['logVerbosity'];
connected = json['connected'] ?? false;
status = json['status'] ?? "";
errors = [];
List<dynamic> rawErrors = json["errors"];
rawErrors.forEach((error) {
errors.add(error);
});
_updates = EventChannel('net.defined.nebula/$id');
_updates.receiveBroadcastStream().listen((d) {
try {
this.status = d['status'];
this.connected = d['connected'];
_change.add(null);
} catch (err) {
//TODO: handle the error
print(err);
}
}, onError: (err) {
var error = err as PlatformException;
this.status = error.details['status'];
this.connected = error.details['connected'];
_change.addError(error.message);
});
}
Stream onChange() {
return _change.stream;
}
Map<String, dynamic> toJson() {
return {
'name': name,
'id': id,
'staticHostmap': staticHostmap,
'unsafeRoutes': unsafeRoutes,
'ca': ca?.map((cert) {
return cert.rawCert;
})?.join('\n') ??
"",
'cert': cert?.rawCert,
'key': key,
'lhDuration': lhDuration,
'port': port,
'mtu': mtu,
'cipher': cipher,
'sortKey': sortKey,
'logVerbosity': logVerbosity,
};
}
save() async {
try {
var raw = jsonEncode(this);
await platform.invokeMethod("saveSite", raw);
} on PlatformException catch (err) {
//TODO: fix this message
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
Future<String> renderConfig() async {
try {
var raw = jsonEncode(this);
return await platform.invokeMethod("nebula.renderConfig", raw);
} on PlatformException catch (err) {
//TODO: fix this message
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
start() async {
try {
await platform.invokeMethod("startSite", <String, String>{"id": id});
} on PlatformException catch (err) {
//TODO: fix this message
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
stop() async {
try {
await platform.invokeMethod("stopSite", <String, String>{"id": id});
} on PlatformException catch (err) {
//TODO: fix this message
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
Future<List<HostInfo>> listHostmap() async {
try {
var ret = await platform.invokeMethod("active.listHostmap", <String, String>{"id": id});
List<dynamic> f = jsonDecode(ret);
List<HostInfo> hosts = [];
f.forEach((v) {
hosts.add(HostInfo.fromJson(v));
});
return hosts;
} on PlatformException catch (err) {
//TODO: fix this message
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
Future<List<HostInfo>> listPendingHostmap() async {
try {
var ret = await platform.invokeMethod("active.listPendingHostmap", <String, String>{"id": id});
List<dynamic> f = jsonDecode(ret);
List<HostInfo> hosts = [];
f.forEach((v) {
hosts.add(HostInfo.fromJson(v));
});
return hosts;
} on PlatformException catch (err) {
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
Future<Map<String, List<HostInfo>>> listAllHostmaps() async {
try {
var res = await Future.wait([this.listHostmap(), this.listPendingHostmap()]);
return {"active": res[0], "pending": res[1]};
} on PlatformException catch (err) {
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
void dispose() {
_change.close();
}
Future<HostInfo> getHostInfo(String vpnIp, bool pending) async {
try {
var ret = await platform.invokeMethod("active.getHostInfo", <String, dynamic>{"id": id, "vpnIp": vpnIp, "pending": pending});
final h = jsonDecode(ret);
if (h == null) {
return null;
}
return HostInfo.fromJson(h);
} on PlatformException catch (err) {
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
Future<HostInfo> setRemoteForTunnel(String vpnIp, String addr) async {
try {
var ret = await platform.invokeMethod("active.setRemoteForTunnel", <String, dynamic>{"id": id, "vpnIp": vpnIp, "addr": addr});
final h = jsonDecode(ret);
if (h == null) {
return null;
}
return HostInfo.fromJson(h);
} on PlatformException catch (err) {
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
Future<bool> closeTunnel(String vpnIp) async {
try {
return await platform.invokeMethod("active.closeTunnel", <String, dynamic>{"id": id, "vpnIp": vpnIp});
} on PlatformException catch (err) {
throw err.details ?? err.message ?? err.toString();
} catch (err) {
throw err.toString();
}
}
}

View File

@ -0,0 +1,28 @@
import 'IPAndPort.dart';
class StaticHost {
bool lighthouse;
List<IPAndPort> destinations;
StaticHost({this.lighthouse, this.destinations});
StaticHost.fromJson(Map<String, dynamic> json) {
lighthouse = json['lighthouse'];
var list = json['destinations'] as List<dynamic>;
var result = List<IPAndPort>();
list.forEach((item) {
result.add(IPAndPort.fromString(item));
});
destinations = result;
}
Map<String, dynamic> toJson() {
return {
'lighthouse': lighthouse,
'destinations': destinations,
};
}
}

View File

@ -0,0 +1,18 @@
class UnsafeRoute {
String route;
String via;
UnsafeRoute({this.route, this.via});
UnsafeRoute.fromJson(Map<String, dynamic> json) {
route = json['route'];
via = json['via'];
}
Map<String, dynamic> toJson() {
return {
'route': route,
'via': via,
};
}
}

Some files were not shown because too many files have changed in this diff Show More