diff --git a/.gitignore b/.gitignore index 790fc72..77d7b33 100755 --- a/.gitignore +++ b/.gitignore @@ -41,5 +41,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/app/build +/android/.gradle local diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f2a6a10 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright NodeBase with all contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 25a04c4..312f37f 100755 --- a/README.md +++ b/README.md @@ -1,155 +1,18 @@ -# nodebase - - - -Running Node.js application over Wifi and share with your friends. - -For previous mature version, please explore source code on - kotlin branch and - v0flutter branch. - -## Rewrite Progress - -[x] migrate kotlin service code -[x] implement windows adapter for MethodChannel and EventChannel -[ ] rewrite new design UI in dart -[ ] implement linux adapter -[ ] implement macosx adapter -[ ] implement web+ios adapter - -## How to use - -- Platform/App market is online - - click into platform or application page - - click on the top-right cart icon button - - select what you want to download - - e.g. download platform `node-10.10.0` - - download app `file-transfer` - - edit app `file-transfer` platform value to `node-10.10.0` - - start `file-transfer` by clicking play icon button -- Create a new platform, for example named `node` - - fill node url like `file:///sdcard/bin-node-v10.10.0` or `https://example.com/latest/arm/node` - - click download button and wait for task complete - - (NodeBase will copy the binary to its app zone and make it executable) - - Wow; now we not only support node binary but also customized exectuables. -- Create a new app, for example named `test` and its platform is `node` - - click into the new app - - download an app zip into for example `/sdcard/test.zip` - - fill `Import / Export` text field with `/sdcard/test.zip` - - click upload button and wait for task complete - - (NodeBase will extract zip app as a app folder into app zone) - - fill `Params` text field (for example, file manager need to config target folder as first param) - - click `play` button to start node app - - click `open in browser` button to open the app in a webview / `pop-out` button to open in external browser - - click `stop` button to stop node app - -### App folder structure - -``` -//config.json -{ - "host": "http://127.0.0.1" - "port": 0, - "home": "/index.html", - "entry": "index.js" -} - -//static/index.html -[...] source code frontend client - -//index.js -ref: https://github.com/stallpool/halfbase/tree/master/nodejs/tinyserver/index.js -[...] source code for backend server -[...] hook `/index.html` to load `/app/static/index.html` -``` - -App examples: [https://github.com/nodebase0](https://github.com/nodebase0), includes file-viewer-uploader, nodepad, ... - - -## Development - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - -##### NodeJS/Python binary for ARM - -ref: https://github.com/dna2github/dna2oslab/releases/tag/0.2.0-android-gt6-arm - -##### Java binary - -write a shell script `java` and `adb push java /sdcard` -``` -#!/system/bin/sh - -exec dalvikvm $@ -``` - -create a new platform in NodeBase and download java wrapper from `file:///sdcard/java` - -then write a command line tool to have a try. ref: https://github.com/dna2github/dna2sevord/tree/master/past/others/walkserver/javacmd - -##### Golang binary - -``` -# download go source package and extract -cd src -GOOS=android GOARCH=arm64 ./bootstrap.bash - -tar zcf go-android-arm64-bootstrap.tar.gz go-android-arm64-bootstrap -adb push go-android-arm64-bootstrap.tar.gz /sdcard/ -# we suggest write a javascript script to set up golang environment on your Android -# to extract tar package to NodeBase app zone /data/user/0/net.seven.nodebase/ -# e.g. /data/user/0/net.seven.nodebase/go-android-arm64-bootstrap -``` - -write a shell script `go` and `adb push go /sdcard` - -``` -#!/system/bin/sh - -SELF=$(cd `dirname $0`; pwd) -BASE=/data/user/0/net.seven.nodebase/go-android-arm64-bootstrap -CACHEBASE=${BASE}/cache -mkdir -p ${CACHEBASE}/{cache,tmp,local} -export GOROOT=${BASE} -export GOPATH=${CACHEBASE}/golang/local -export GOCACHE=${CACHEBASE}/golang/cache -export GOTMPDIR=${CACHEBASE}/golang/tmp -export CGO_ENABLED=0 -exec ${BASE}/bin/go run $@ -``` - -create a new platform in NodeBase and download go wrapper from `file:///sdcard/go`; - -then write a tiny server to have a try. ref: https://github.com/stallpool/halfbase/blob/master/golang/tinyserver/main.go - -##### Notice - -currently NodeBase support kill a program with 1-level children, for example `go run main.go` will spawn a child process `main`; -if click on `stop` button, NodeBase can kill the `go run` and its child `main`. - -if remove `exec` in the `go` wrapper shell script, the shell script will run in `sh`, it spawn `go run` and the `go run` spawn `main`; -when `stop` the application, NodeBase will merely kill `sh` and its child `go run`; but `main` will still be running there, -which may cause next `start` failure (like port has already been used) and need to kill whole NodeBase for cleanup. - -## Flutter Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - +# NodeBase + + +Platform to Build Sharable Application for Android + +Running Websocket application over Wifi and share with your friends. + +> WSTun: it is a base server on http/websocket with netty; and project name is "WSTun" + +The design is changed, we never use a pre-built binary of NodeJS to run server anymore. + +We run a native http/websocket with netty. + +Any JS client can register itself as a service so that it can be a control center of data. + +Others can connect as clients of the service and do more flexible things like file sharing, board gaming! + +> It is also a project enjoy vibe coding! (save lots of time) diff --git a/analysis_options.yaml b/analysis_options.yaml deleted file mode 100755 index d4e0f0c..0000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100755 index 5d99765..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app -key.properties -**/*.keystore -**/*.jks diff --git a/android/README.md b/android/README.md new file mode 100644 index 0000000..8f633da --- /dev/null +++ b/android/README.md @@ -0,0 +1,166 @@ +# WSTun - WebSocket Tunnel Server + +An Android app that runs an HTTP/WebSocket server for service sharing in local networks. + +## Features + +### Server (Android App) +- HTTP server with WebSocket support using Netty +- Optional HTTPS with self-signed certificate +- Service registration via WebSocket +- HTTP request relay to connected services +- Foreground service for background operation +- Service management UI (view, kick) + +### Services +- Connect via WebSocket and register endpoints +- Define HTTP routes that relay to the service +- Provide static resources (HTML/JS/CSS) +- No server-side storage - everything relayed + +## Architecture + +``` +┌─────────────┐ WebSocket ┌─────────────────┐ +│ Service │◄───────────────────►│ WSTun App │ +│ Client │ │ (HTTP Server) │ +└─────────────┘ └────────┬────────┘ + │ HTTP + ▼ + ┌─────────────────┐ + │ Web Browser │ + │ (User) │ + └─────────────────┘ +``` + +1. Service connects via WebSocket and registers +2. User accesses `http://server/[service]/main` +3. Server relays request to service via WebSocket +4. Service sends response via WebSocket +5. Server sends HTTP response to user + +## Building + +### Android App + +```bash +cd wstun +./gradlew assembleDebug +``` + +### Service Clients + +```bash +cd services/fileshare +npm install + +cd services/chat +npm install +``` + +## Usage + +### Start the Server + +1. Install the APK on an Android device +2. Configure port and HTTPS option +3. Tap "Start Server" +4. Note the displayed IP address + +### Connect Services + +```bash +# File sharing +cd services/fileshare +node client.js ws://192.168.1.100:8080/ws + +# Chat +cd services/chat +node client.js ws://192.168.1.100:8080/ws +``` + +### Access Services + +- Server info: `http://192.168.1.100:8080/` +- FileShare: `http://192.168.1.100:8080/fileshare/main` +- Chat: `http://192.168.1.100:8080/chat/main` + +## Protocol + +### Service Registration + +```json +{ + "type": "register", + "id": "unique-id", + "payload": { + "name": "servicename", + "type": "service-type", + "description": "Service description", + "endpoints": [ + { "path": "/main", "method": "GET", "relay": true } + ], + "static_resources": { + "/main": "..." + } + } +} +``` + +### HTTP Request Relay + +Server sends to service: +```json +{ + "type": "http_request", + "payload": { + "request_id": "req-123", + "method": "GET", + "path": "/servicename/main", + "headers": { "Content-Type": "text/html" }, + "body": "..." + } +} +``` + +Service responds: +```json +{ + "type": "http_response", + "payload": { + "request_id": "req-123", + "status": 200, + "headers": { "Content-Type": "text/html" }, + "body": "..." + } +} +``` + +## Included Services + +### FileShare + +Share files without server storage: +- Virtual folder tree +- File upload/download via browser +- Relay mode for temporary sharing +- All data flows through the client + +### Chat + +Real-time chat with rich messages: +- Text, Card, and Poll message types +- Broadcast to all or private messages +- Abstract JSON format for custom rendering +- Self-contained HTML/JS/CSS + +## Security Notes + +- HTTPS uses self-signed certificate (browser warning expected) +- No authentication built-in (add as needed) +- For local network use only +- Services should validate inputs + +## License + +MIT diff --git a/android/app/build.gradle b/android/app/build.gradle old mode 100755 new mode 100644 index 4a42cf3..334f5b2 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,57 +1,48 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "net.seven.nodebase.nodebase" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "net.seven.nodebase.nodebase" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} +plugins { + id 'com.android.application' +} + +android { + namespace 'seven.lab.wstun' + compileSdk 34 + + defaultConfig { + applicationId "seven.lab.wstun" + minSdk 24 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + packagingOptions { + exclude 'META-INF/INDEX.LIST' + exclude 'META-INF/io.netty.versions.properties' + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation 'com.google.code.gson:gson:2.10.1' + + // Netty + implementation 'io.netty:netty-all:4.1.100.Final' + + // Bouncy Castle for SSL + implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..d52bf27 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,20 @@ +# Netty +-keepclassmembers class io.netty.** { *; } +-keep class io.netty.** { *; } +-dontwarn io.netty.** + +# Bouncy Castle +-keep class org.bouncycastle.** { *; } +-dontwarn org.bouncycastle.** + +# Gson +-keepattributes Signature +-keepattributes *Annotation* +-keep class com.google.gson.** { *; } +-keep class * implements java.io.Serializable { *; } + +# Protocol classes +-keep class seven.lab.wstun.protocol.** { *; } + +# Server classes +-keep class seven.lab.wstun.server.** { *; } diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100755 index 8ffe024..0000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml old mode 100755 new mode 100644 index a6b5ed6..9d0275f --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,51 +1,43 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/assets/libwstun.js b/android/app/src/main/assets/libwstun.js new file mode 100644 index 0000000..a014927 --- /dev/null +++ b/android/app/src/main/assets/libwstun.js @@ -0,0 +1,274 @@ +/** + * WSTun Client Library v2.0 + * Supports: Server auth, Service instances, User management + */ +(function(global) { +'use strict'; + +const WSTun = { + version: '1.0.0', + + /** Create instance host (creates and manages an instance) */ + createInstanceHost: function(options) { return new InstanceHost(options); }, + + /** Create instance client (joins an existing instance) */ + createInstanceClient: function(options) { return new InstanceClient(options); }, + + /** Generate unique ID */ + generateId: function() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 6); }, + + /** Build WebSocket URL */ + buildWsUrl: function(serverToken) { + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + let url = protocol + '//' + location.host + '/ws'; + if (serverToken) url += '?token=' + encodeURIComponent(serverToken); + return url; + } +}; + +/** Base WebSocket client */ +class BaseClient { + constructor(options) { + this.service = options.service; + this.serverToken = options.serverToken || null; + this.onOpen = options.onOpen || (() => {}); + this.onMessage = options.onMessage || (() => {}); + this.onClose = options.onClose || (() => {}); + this.onError = options.onError || (() => {}); + this.ws = null; + this.connected = false; + } + + connect() { + try { this.ws = new WebSocket(WSTun.buildWsUrl(this.serverToken)); } + catch (err) { this.onError(err); return; } + this.ws.onopen = () => { this.connected = true; this.onOpen(); this._onConnected(); }; + this.ws.onmessage = (e) => { try { this._handleMessage(JSON.parse(e.data)); } catch(err) { console.error(err); } }; + this.ws.onclose = () => { this.connected = false; this.onClose(); }; + this.ws.onerror = (err) => { this.onError(err); }; + } + + disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } } + + send(type, payload) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify({ type, service: this.service, payload })); + } + } + + _onConnected() {} + _handleMessage(msg) { this.onMessage(msg); } +} + +/** + * Instance Host - Creates and manages a service instance (room) + * @param {Object} options + * @param {string} options.service - Service type (e.g., 'fileshare', 'chat') + * @param {string} options.name - Instance name (room name) + * @param {string} [options.serverToken] - Server auth token + * @param {string} [options.instanceToken] - Token users need to join + * @param {string} [options.reclaimUuid] - UUID of instance to reclaim (for reconnection) + * @param {function} [options.onInstanceCreated] - Called when instance is created + * @param {function} [options.onInstanceReclaimed] - Called when instance is reclaimed + */ +class InstanceHost extends BaseClient { + constructor(options) { + super(options); + this.instanceName = options.name || 'Room'; + this.instanceToken = options.instanceToken || null; + this.reclaimUuid = options.reclaimUuid || null; + this.onInstanceCreated = options.onInstanceCreated || (() => {}); + this.onInstanceReclaimed = options.onInstanceReclaimed || options.onInstanceCreated || (() => {}); + this.instanceUuid = null; + } + + _onConnected() { + if (this.reclaimUuid) { + // Try to reclaim an existing instance + this.send('reclaim_instance', { + uuid: this.reclaimUuid, + token: this.instanceToken, + server_token: this.serverToken + }); + } else { + // Create new instance + this.send('create_instance', { + name: this.instanceName, + token: this.instanceToken, + server_token: this.serverToken + }); + } + } + + _handleMessage(msg) { + if (msg.type === 'instance_created' && msg.payload?.success) { + this.instanceUuid = msg.payload.uuid; + this.onInstanceCreated(msg.payload); + } else if (msg.type === 'instance_reclaimed' && msg.payload?.success) { + this.instanceUuid = msg.payload.uuid; + this.onInstanceReclaimed(msg.payload); + } else if (msg.type === 'instance_reclaimed' && !msg.payload?.success) { + // Reclaim failed, create new instance instead + this.reclaimUuid = null; + this.send('create_instance', { + name: this.instanceName, + token: this.instanceToken, + server_token: this.serverToken + }); + } else if (msg.type === 'error') { + this.onError(new Error(msg.payload?.message || 'Unknown error')); + } else { + this.onMessage(msg); + } + } + + /** Broadcast message to all users in instance */ + broadcast(type, payload) { + if (this.instanceUuid) { + payload = payload || {}; + payload.instanceUuid = this.instanceUuid; + this.send(type, payload); + } + } + + /** Kick a user from instance */ + kickUser(userId) { + this.send('kick_client', { userId, instanceUuid: this.instanceUuid }); + } +} + +/** + * Instance Client - Joins an existing instance + * @param {Object} options + * @param {string} options.service - Service type + * @param {string} options.instanceUuid - Instance UUID to join + * @param {string} [options.serverToken] - Server auth token + * @param {string} [options.instanceToken] - Instance access token + * @param {string} [options.userId] - User ID (auto-generated if not provided) + * @param {function} [options.onJoined] - Called when joined instance + * @param {function} [options.onKicked] - Called when kicked from instance + */ +class InstanceClient extends BaseClient { + constructor(options) { + super(options); + this.instanceUuid = options.instanceUuid; + this.instanceToken = options.instanceToken || null; + this.userId = options.userId || WSTun.generateId(); + this.onJoined = options.onJoined || (() => {}); + this.onKicked = options.onKicked || (() => {}); + this.instanceName = null; + } + + _onConnected() { + this.send('join_instance', { + uuid: this.instanceUuid, + userId: this.userId, + token: this.instanceToken + }); + } + + _handleMessage(msg) { + if (msg.type === 'ack' && msg.payload?.success && msg.payload?.instanceUuid) { + this.instanceName = msg.payload.instanceName; + this.onJoined(msg.payload); + } else if (msg.type === 'kick') { + this.onKicked(msg.payload); + this.disconnect(); + } else if (msg.type === 'error') { + this.onError(new Error(msg.payload?.message || 'Unknown error')); + } else { + this.onMessage(msg); + } + } +} + +/** + * List available instances for a service + * @param {string} service - Service type + * @param {string} [serverToken] - Server auth token + * @returns {Promise} List of instances + */ +WSTun.listInstances = function(service, serverToken) { + return new Promise((resolve, reject) => { + let resolved = false; + let ws; + + try { + ws = new WebSocket(WSTun.buildWsUrl(serverToken)); + } catch(err) { + reject(err); + return; + } + + const cleanup = () => { + resolved = true; + if (ws && ws.readyState !== WebSocket.CLOSED) { + try { ws.close(); } catch(e) {} + } + }; + + ws.onopen = () => { + if (resolved) return; + ws.send(JSON.stringify({ type: 'list_instances', service, payload: {} })); + }; + ws.onmessage = (e) => { + if (resolved) return; + try { + const msg = JSON.parse(e.data); + if (msg.type === 'instance_list') { + cleanup(); + resolve(msg.payload?.instances || []); + } else if (msg.type === 'error') { + cleanup(); + reject(new Error(msg.payload?.message || msg.payload?.error || 'Failed to list instances')); + } + } catch(err) { + cleanup(); + reject(err); + } + }; + ws.onerror = (err) => { + if (resolved) return; + cleanup(); + reject(new Error('WebSocket connection failed')); + }; + ws.onclose = () => { + if (resolved) return; + cleanup(); + resolve([]); // Return empty list if connection closes without response + }; + + // Shorter timeout (3 seconds) to avoid long waits + setTimeout(() => { + if (resolved) return; + cleanup(); + resolve([]); // Return empty list on timeout instead of rejecting + }, 3000); + }); +}; + +// Legacy support - createService and createClient still work for simple cases +WSTun.createService = function(options) { return new InstanceHost(options); }; +WSTun.createClient = function(options) { + // If instanceUuid provided, use InstanceClient, otherwise fallback + if (options.instanceUuid) return new InstanceClient(options); + // Simple client without instance support (legacy) + const client = new BaseClient(options); + client.userId = options.userId || WSTun.generateId(); + client.onRegistered = options.onRegistered || (() => {}); + client.onKicked = options.onKicked || (() => {}); + client._onConnected = function() { + this.send('client_register', { clientType: this.service, userId: this.userId, auth_token: options.serviceToken }); + }; + client._handleMessage = function(msg) { + if (msg.type === 'ack' && msg.payload?.success) { this.onRegistered(msg.payload); } + else if (msg.type === 'kick') { this.onKicked(msg.payload); this.disconnect(); } + else if (msg.type === 'error') { this.onError(new Error(msg.payload?.message || 'Error')); } + else { this.onMessage(msg); } + }; + return client; +}; +WSTun.generateUserId = WSTun.generateId; + +global.WSTun = WSTun; +})(typeof window !== 'undefined' ? window : this); diff --git a/android/app/src/main/assets/services/chat/index.html b/android/app/src/main/assets/services/chat/index.html new file mode 100644 index 0000000..86aa902 --- /dev/null +++ b/android/app/src/main/assets/services/chat/index.html @@ -0,0 +1,274 @@ + + + + + + Chat Instance Manager - WSTun + + + +
+
+

Chat

+

Create a chat room

+ +
+ + +
+
+ + +
+
+ + +
+ + + + +
+ +
+

Existing Rooms

+

Click to open the chat page

+
+

Loading...

+
+ +
+
+ + + + + diff --git a/android/app/src/main/assets/services/chat/main.html b/android/app/src/main/assets/services/chat/main.html new file mode 100644 index 0000000..7d2fac6 --- /dev/null +++ b/android/app/src/main/assets/services/chat/main.html @@ -0,0 +1,375 @@ + + + + + + Chat - WSTun + + + +
+

Join Chat Room

+
Loading rooms...
+ +
+ +
+
Or enter room details manually
+ + + + +
+
+ +
+
+
+

Chat Room

+ 0 users +
+ +
+
+
+ + +
+
+ + + + + diff --git a/android/app/src/main/assets/services/fileshare/index.html b/android/app/src/main/assets/services/fileshare/index.html new file mode 100644 index 0000000..a350c59 --- /dev/null +++ b/android/app/src/main/assets/services/fileshare/index.html @@ -0,0 +1,276 @@ + + + + + + FileShare Instance Manager - WSTun + + + +
+
+

FileShare

+

Create a file sharing room

+ +
+ + +
+
+ + +
+
+ + +
+ + + + +
+ +
+

Existing Rooms

+

Click to open the file sharing page

+
+

Loading...

+
+ +
+
+ + + + + diff --git a/android/app/src/main/assets/services/fileshare/main.html b/android/app/src/main/assets/services/fileshare/main.html new file mode 100644 index 0000000..2ddb8d8 --- /dev/null +++ b/android/app/src/main/assets/services/fileshare/main.html @@ -0,0 +1,463 @@ + + + + + + FileShare - WSTun + + + +
+
+

FileShare

+

Share files with others

+
Loading...
+
+ + +
+

Join a Room

+
+

Loading available rooms...

+
+
+

Or Enter Room Details

+
+ + +
+
+ + +
+ +
+
+ + + +
+ + + + + diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/ChannelResult.java b/android/app/src/main/java/net/seven/nodebase/nodebase/ChannelResult.java deleted file mode 100755 index f661a24..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/ChannelResult.java +++ /dev/null @@ -1,6 +0,0 @@ -package net.seven.nodebase.nodebase; - -public enum ChannelResult { - OK, - FAILED -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/Command.java b/android/app/src/main/java/net/seven/nodebase/nodebase/Command.java deleted file mode 100755 index 24ee694..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/Command.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.seven.nodebase.nodebase; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.util.ArrayList; - -public class Command { - public static String checkOutput(String[] cmd) { - return checkOutput(cmd, false); - } - public static String checkOutput(String[] cmd, boolean joinStderr) { - try { - ProcessBuilder build = new ProcessBuilder(cmd); - if (joinStderr) build.redirectErrorStream(true); - Process p = build.start(); - BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - sb.append('\n'); - } - p.waitFor(); - reader.close(); - return sb.toString().substring(0, sb.length() - 1); - } catch(Exception e) { - Logger.d( - "NodeBase", - "command", - String.format( - "%s :: %s", - String.join(" ", cmd), - e.toString() - ) - ); - return null; - } - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/External.java b/android/app/src/main/java/net/seven/nodebase/nodebase/External.java deleted file mode 100755 index 61f161c..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/External.java +++ /dev/null @@ -1,49 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import java.io.File; - -public class External { - public static void openBrowser(Context context, String url) { - Intent it = new Intent(Intent.ACTION_SEND); - it.setAction("android.intent.action.VIEW"); - it.setData(Uri.parse(url)); - context.startActivity(it); - } - - public static void shareInformation( - Context context, - String title, - String subtitle, - String text - ) { - shareInformation(context, title, subtitle, text, null); - } - public static void shareInformation( - Context context, - String title, - String subtitle, - String text, - String imgFilePath - ) { - Intent it = new Intent(Intent.ACTION_SEND); - if (imgFilePath == null || imgFilePath.length() == 0) { - it.setType("text/plain"); - } else { - File f = new File(imgFilePath); - if (f.exists() && f.isFile()) { - it.setType("image/jpg"); - it.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f)); - } else { - it.setType("text/plain"); - } - } - if (subtitle != null) it.putExtra(Intent.EXTRA_SUBJECT, subtitle); - if (text != null) it.putExtra(Intent.EXTRA_TEXT, text); - it.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(Intent.createChooser(it, title)); - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/ForegroundNodeService.java b/android/app/src/main/java/net/seven/nodebase/nodebase/ForegroundNodeService.java deleted file mode 100755 index 23c7702..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/ForegroundNodeService.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.IBinder; - -import androidx.core.app.NotificationCompat; - -public class ForegroundNodeService extends Service { - private static final String CHANNEL_ID = "net.seven.nodebase.foregroundservice"; - public static final String ARGV = "NodeService"; - - public ForegroundNodeService() { } - - private void registerNotificationItem() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel srvChannel = new NotificationChannel( - CHANNEL_ID, - "NodeBase Service", - NotificationManager.IMPORTANCE_DEFAULT - ); - NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(srvChannel); - } - - Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("NodeBase") - .setContentText("application nodebase service running ...") - .build(); - startForeground(1, notification); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - registerNotificationItem(); - return Service.START_STICKY; - } - - @Override - public void onCreate() { - super.onCreate(); - } - - @Override - public void onDestroy() { - NodeAppService.stopNodeApps(); - super.onDestroy(); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } -} \ No newline at end of file diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/Logger.java b/android/app/src/main/java/net/seven/nodebase/nodebase/Logger.java deleted file mode 100755 index 762f0a8..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/Logger.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.util.Log; - -public class Logger { - public static void i(String tag, String sub, String message) { - Log.i(tag, String.format("[I] %s :: %s", sub, message)); - } - - public static void w(String tag, String sub, String message) { - Log.w(tag, String.format("[W] %s :: %s", sub, message)); - } - - public static void e(String tag, String sub, String message) { - Log.e(tag, String.format("[E] %s :: %s", sub, message)); - } - - public static void d(String tag, String sub, String message) { - Log.d(tag, String.format("[D] %s :: %s", sub, message)); - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/MainActivity.java b/android/app/src/main/java/net/seven/nodebase/nodebase/MainActivity.java deleted file mode 100755 index 7510078..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/MainActivity.java +++ /dev/null @@ -1,151 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.content.Intent; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - listenNodeAppChannel(flutterEngine); - } - - private void listenNodeAppChannel(@NonNull FlutterEngine flutterEngine) { - new MethodChannel( - flutterEngine.getDartExecutor().getBinaryMessenger(), - "net.seven.nodebase/app" - ).setMethodCallHandler(new MethodChannel.MethodCallHandler() { - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - switch(call.method) { - case "app.start" -> { - String name = call.argument("name"); - String cmd = call.argument("cmd"); - if (name == null || cmd == "" || cmd.length() == 0) { - result.error("app.start", "empty name or cmd", null); - return; - } - if (startNodeApp(name, cmd) == ChannelResult.OK) { - result.success(true); - } else { - result.error("app.start", "internal error", null); - } - } - case "app.stop" -> { - String name = call.argument("name"); - if (name == null) { - result.error("app.stop", "empty name", null); - return; - } - if (stopNodeApp(name) == ChannelResult.OK) { - result.success(true); - } else { - result.error("app.stop", "internal error", null); - } - } - case "app.stat" -> { - String name = call.argument("name"); - if (name == null) { - result.error("app.stat", "empty name", null); - return; - } - var stat = getNodeAppStatus(name); - if (stat == null) { - result.error("app.stat", "no stat", null); - return; - } - result.success(stat); - } - case "util.ip" -> { - var ipJsonString = getIPs(); - result.success(ipJsonString); - } - case "util.browser.open" -> { - String url = call.argument("url"); - if (url == null) { - result.error("util.browser", "no url", null); - return; - } - External.openBrowser(MainActivity.this.getContext(), url); - result.success(true); - } - case "util.file.executable" -> { - String fname = call.argument("filename"); - if (fname == null) { - result.error("util.file.executable", "no filename", null); - return; - } - fileExecutablize(fname); - result.success(true); - } - } - } - }); - - new EventChannel( - flutterEngine.getDartExecutor().getBinaryMessenger(), - "net.seven.nodebase/event" - ).setStreamHandler(NodeAppService.getEventHandler()); - } - - public ChannelResult startNodeApp(String name, String cmd) { - Logger.i( - "NodeBase", - "startNodeApp", - String.format("start node app (%s) -> %s", name, cmd)); - Intent it = new Intent(getContext(), ForegroundNodeService.class); - ContextCompat.startForegroundService(getContext(), it); - NodeAppService.startNodeApp(name, cmd.split("\u0001")); - return ChannelResult.OK; - } - - public ChannelResult stopNodeApp(String name) { - Logger.i( - "NodeBase", - "stopNodeApp", - String.format("stop node app (%s)", name)); - Intent it = new Intent(getContext(), ForegroundNodeService.class); - if (NodeAppService.getRunningNodeAppCount() <= 1) { - stopService(it); - } else { - NodeAppService.stopNodeApp(name); - } - return ChannelResult.OK; - } - - public HashMap getNodeAppStatus(String name) { - // TODO get NodeMonitor and assemble data as json - NodeAppMonitor app = NodeAppService.getNodeApp(name); - HashMap json = new HashMap<>(); - String state = "none"; - if (app != null) { - if (app.nodebaseIsReady()) state = "new"; - else if (app.nodebaseIsRunning()) state = "running"; - else if (app.nodebaseIsDead()) state = "dead"; - json.put("state", state); - } - return json; - } - - public HashMap> getIPs() { - var json = Network.getIPs(); - return json; - } - - public boolean fileExecutablize(String fname) { - File f = new File(fname); - return f.setExecutable(true, true); - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/Network.java b/android/app/src/main/java/net/seven/nodebase/nodebase/Network.java deleted file mode 100755 index 0f9b361..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/Network.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.seven.nodebase.nodebase; - -import java.net.InterfaceAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -public class Network { - public static HashMap> getIPs() { - return getIPsFromAndroidApi(); - } - - public static HashMap> getIPsFromAndroidApi() { - HashMap> r = new HashMap<>(); - try { - for (NetworkInterface nic : Collections.list(NetworkInterface.getNetworkInterfaces())) { - String nic_name = nic.getName(); - List nic_addr = nic.getInterfaceAddresses(); - ArrayList ips = new ArrayList<>(); - for (InterfaceAddress ia : nic_addr) { - String addr = ia.getAddress().getHostAddress(); - // skip the address % - if (addr == null || addr.indexOf('%') >= 0) continue; - ips.add(addr); - } - if (ips.size() > 0) r.put(nic_name, ips); - } - Logger.d( - "NodeBase", - "getIPsFromAndroidApi", - r.toString() - ); - } catch (SocketException e) { - Logger.e( - "NodeBase", - "getIPsFromAndroidApi", - e.toString() - ); - } - return r; - } - -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppMonitor.java b/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppMonitor.java deleted file mode 100755 index 2a0da5d..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppMonitor.java +++ /dev/null @@ -1,197 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.os.Build; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Locale; - -public class NodeAppMonitor extends Thread { - private enum STAT { - UNKNOWN, - BORN, - READY, - RUNNING, - DEAD - } - - private STAT nodebaseStat; - private Process nodebaseProcess; - private final String nodebaseName; - private final String[] nodebaseCmd; - - public NodeAppMonitor(String name, String[] cmd) { - super(); - this.nodebaseName = name; - this.nodebaseCmd = cmd; - this.nodebaseStat = STAT.BORN; - this.nodebaseProcess = null; - } - - public boolean nodebaseIsRunning() { return STAT.RUNNING == this.nodebaseStat; } - public boolean nodebaseIsReady() { return STAT.READY == this.nodebaseStat; } - public boolean nodebaseIsDead() { return STAT.DEAD == this.nodebaseStat; } - - private int nodebasePid() { - Process p = this.nodebaseProcess; - if (p == null) return -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (!p.isAlive()) return -1; - } - Class k = p.getClass(); - if ("java.lang.UNIXProcess".equals(k.getName())) { - try { - int pid = -1; - Field f = k.getDeclaredField("pid"); - f.setAccessible(true); - // this try to make sure if getInt throw an error, - // `setAccessible(false)` can be executed - // so that `pid` is protected after this access - try { pid = f.getInt(p); } catch (IllegalAccessException e) { } - f.setAccessible(false); - return pid; - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - return -1; - } - - private ArrayList nodebaseChildrenPids(int pid) { - // XXX maybe we can get sub processes pids from a stable way like /proc - // for a workaround, currently we just use `ps` - ArrayList children = new ArrayList<>(); - String output = Command.checkOutput(new String[] { - "/system/bin/ps", - "-o", "pid=", "--pid", String.valueOf(pid) - }); - if (output == null || output.length() == 0) return children; - String[] lines = output.trim().split("\n"); - for(String line : lines) { - line = line.trim(); - if (line.length() == 0) continue; - // TODO: try...catch... - int cpid = Integer.parseInt(line); - if (pid != cpid) children.add(cpid); - } - return children; - } - - private int[] nodebaseCollectPidsForKilling(ArrayList collected, int pid, boolean nested) { - if (pid < 0) return new int[0]; - if (collected == null) collected = new ArrayList<>(); - ArrayList pids = nodebaseChildrenPids(pid); - for (int childPid : pids) { - if (childPid < 0) continue; - collected.add(childPid); - Logger.i( - "NodeBase", - "monitor", - String.format( - Locale.getDefault(), - "killing (%d / %s) -> %d | parent=%d", - getId(), this.nodebaseName, - childPid, pid) - ); - if (nested) nodebaseCollectPidsForKilling(collected, childPid, true); - } - int n = collected.size(); - int[] r = new int[n]; - for (int i = 0; i < n; i++) { - r[i] = collected.get(i); - } - return r; - } - - public void nodebaseStart() { - this.start(); - } - - public boolean nodebaseStop() { - // XXX only RUNNING app can be stopped to avoid race condition - // stop non-RUNNING app will return false - if (this.nodebaseProcess == null) return false; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (!this.nodebaseProcess.isAlive()) return false; - } - int[] pids = this.nodebaseCollectPidsForKilling(null, this.nodebasePid(), true); - for (int pid : pids) { - android.os.Process.killProcess(pid); - } - this.nodebaseProcess.destroy(); - return true; - } - - public NodeAppMonitor nodebaseRestart() { - this.nodebaseStop(); - NodeAppMonitor m = new NodeAppMonitor(this.nodebaseName, this.nodebaseCmd); - m.start(); - return m; - } - - @Override - public void run() { - try { - this.nodebaseStat = STAT.READY; - // TODO event/onReady - Logger.i( - "NodeBase", - "monitor", - String.format( - Locale.getDefault(), - "starting (%d / %s) - %s", - getId(), this.nodebaseName, - String.join(" ", this.nodebaseCmd)) - ); - - // set RUNNING before set up the process - // when stop the service, it guarantees that no race condition for - // setting nodebaseStat - this.nodebaseStat = STAT.RUNNING; - this.nodebaseProcess = Runtime.getRuntime().exec(this.nodebaseCmd); - // TODO event/onStart - Logger.i( - "NodeBase", - "monitor", - String.format( - Locale.getDefault(), - "started (%d / %s)", - getId(), this.nodebaseName) - ); - this.nodebaseProcess.waitFor(); - } catch (IOException e) { - Logger.e( - "NodeService", - "monitor", - String.format( - Locale.getDefault(), - "io error (%d / %s) - %s", - getId(), this.nodebaseName, e) - ); - this.nodebaseProcess = null; - // TODO event/onError - } catch (InterruptedException e) { - Logger.e( - "NodeService", - "monitor", - String.format( - Locale.getDefault(), - "interrupted error (%d / %s) - %s", - getId(), this.nodebaseName, e) - ); - // TODO event/onError - } finally { - this.nodebaseStat = STAT.DEAD; - // TODO: event/onPost - Logger.e( - "NodeService", - "monitor", - String.format( - Locale.getDefault(), - "stopped (%d / %s)", - getId(), this.nodebaseName) - ); - } - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppService.java b/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppService.java deleted file mode 100755 index 108d8e5..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppService.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.seven.nodebase.nodebase; - -import java.util.HashMap; - -public class NodeAppService { - private static final NodeBaseEventHandler eventHandler = new NodeBaseEventHandler(); - private static final HashMap services = new HashMap<>(); - - public static NodeBaseEventHandler getEventHandler() { - return eventHandler; - } - - public static int getRunningNodeAppCount() { - int count = 0; - for (NodeAppMonitor app : services.values()) { - if (app != null && !app.nodebaseIsDead()) count ++; - } - return count; - } - - public static void startNodeApp(String name, String[] cmd) { - synchronized (services) { - NodeAppMonitor app; - if (services.containsKey(name)) { - app = services.get(name); - if (app != null && !app.nodebaseIsDead()) return; - // only process if the service is null/dead - } - app = new NodeAppMonitor(name, cmd); - app.nodebaseStart(); - services.put(name, app); - } - } - - public static void restartNodeApp(String name) { - synchronized (services) { - if (!services.containsKey(name)) return; - NodeAppMonitor app = services.get(name); - if (app == null) { - services.remove(name); - return; - } - services.put(name, app.nodebaseRestart()); - } - } - - public static void stopNodeApp(String name) { - synchronized (services) { - if (!services.containsKey(name)) return; - NodeAppMonitor app = services.get(name); - if (app == null) return; - if (app.nodebaseIsDead()) return; - app.nodebaseStop(); - } - } - - public static void stopNodeApps() { - for (String name : services.keySet()) { - stopNodeApp(name); - } - } - - public static NodeAppMonitor getNodeApp(String name) { - if (!services.containsKey(name)) return null; - return services.get(name); - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBase.java b/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBase.java deleted file mode 100755 index 86f311d..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBase.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.seven.nodebase.nodebase; - -public class NodeBase { -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBaseEventHandler.java b/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBaseEventHandler.java deleted file mode 100755 index 9bf4472..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBaseEventHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.seven.nodebase.nodebase; - -import java.util.ArrayList; -import java.util.HashMap; - -import io.flutter.plugin.common.EventChannel; - -public class NodeBaseEventHandler implements EventChannel.StreamHandler { - private HashMap sink = new HashMap<>(); - - @Override - public synchronized void onListen(Object name, EventChannel.EventSink events) { - if (name instanceof String) { - sink.put((String) name, events); - } - } - - @Override - public synchronized void onCancel(Object name) { - if (name instanceof String) { - sink.remove((String) name); - } - } - - public synchronized void postMessage(String name, String message) { - EventChannel.EventSink ch = sink.get(name); - if (ch == null) return; - ch.success(message); - } -} diff --git a/android/app/src/main/java/seven/lab/wstun/config/ServerConfig.java b/android/app/src/main/java/seven/lab/wstun/config/ServerConfig.java new file mode 100644 index 0000000..349a3c2 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/config/ServerConfig.java @@ -0,0 +1,157 @@ +package seven.lab.wstun.config; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * Server configuration stored in SharedPreferences. + */ +public class ServerConfig { + + private static final String PREFS_NAME = "wstun_config"; + private static final String KEY_PORT = "port"; + private static final String KEY_HTTPS_ENABLED = "https_enabled"; + private static final String KEY_CERT_GENERATED = "cert_generated"; + private static final String KEY_CORS_ORIGINS = "cors_origins"; + private static final String KEY_FILESHARE_ENABLED = "fileshare_enabled"; + private static final String KEY_CHAT_ENABLED = "chat_enabled"; + private static final String KEY_SERVER_AUTH_TOKEN = "server_auth_token"; + private static final String KEY_AUTH_ENABLED = "auth_enabled"; + private static final String KEY_DEBUG_LOGS_ENABLED = "debug_logs_enabled"; + + private final SharedPreferences prefs; + + public ServerConfig(Context context) { + this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + public int getPort() { + return prefs.getInt(KEY_PORT, 8080); + } + + public void setPort(int port) { + prefs.edit().putInt(KEY_PORT, port).apply(); + } + + public boolean isHttpsEnabled() { + return prefs.getBoolean(KEY_HTTPS_ENABLED, false); + } + + public void setHttpsEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_HTTPS_ENABLED, enabled).apply(); + } + + public boolean isCertGenerated() { + return prefs.getBoolean(KEY_CERT_GENERATED, false); + } + + public void setCertGenerated(boolean generated) { + prefs.edit().putBoolean(KEY_CERT_GENERATED, generated).apply(); + } + + /** + * Get CORS allowed origins. Default is "*" (all origins). + * Can be comma-separated list of origins or "*". + */ + public String getCorsOrigins() { + return prefs.getString(KEY_CORS_ORIGINS, "*"); + } + + /** + * Set CORS allowed origins. + * Use "*" to allow all origins, or comma-separated list like: + * "http://localhost:3000,http://192.168.1.100:8080" + */ + public void setCorsOrigins(String origins) { + prefs.edit().putString(KEY_CORS_ORIGINS, origins).apply(); + } + + /** + * Check if FileShare service is enabled. + */ + public boolean isFileshareEnabled() { + return prefs.getBoolean(KEY_FILESHARE_ENABLED, false); + } + + /** + * Enable or disable FileShare service. + */ + public void setFileshareEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_FILESHARE_ENABLED, enabled).apply(); + } + + /** + * Check if Chat service is enabled. + */ + public boolean isChatEnabled() { + return prefs.getBoolean(KEY_CHAT_ENABLED, false); + } + + /** + * Enable or disable Chat service. + */ + public void setChatEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_CHAT_ENABLED, enabled).apply(); + } + + /** + * Check if server authentication is enabled. + */ + public boolean isAuthEnabled() { + return prefs.getBoolean(KEY_AUTH_ENABLED, false); + } + + /** + * Enable or disable server authentication. + */ + public void setAuthEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_AUTH_ENABLED, enabled).apply(); + } + + /** + * Get the server auth token. Returns null if not set. + */ + public String getServerAuthToken() { + return prefs.getString(KEY_SERVER_AUTH_TOKEN, null); + } + + /** + * Set the server auth token. Set to null or empty to disable. + */ + public void setServerAuthToken(String token) { + if (token == null || token.isEmpty()) { + prefs.edit().remove(KEY_SERVER_AUTH_TOKEN).apply(); + } else { + prefs.edit().putString(KEY_SERVER_AUTH_TOKEN, token).apply(); + } + } + + /** + * Validate server auth token. + * Returns true if auth is disabled or token matches. + */ + public boolean validateServerAuth(String token) { + if (!isAuthEnabled()) { + return true; + } + String serverToken = getServerAuthToken(); + if (serverToken == null || serverToken.isEmpty()) { + return true; + } + return serverToken.equals(token); + } + + /** + * Check if debug logs endpoint is enabled. + */ + public boolean isDebugLogsEnabled() { + return prefs.getBoolean(KEY_DEBUG_LOGS_ENABLED, false); + } + + /** + * Enable or disable debug logs endpoint. + */ + public void setDebugLogsEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_DEBUG_LOGS_ENABLED, enabled).apply(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/marketplace/InstalledService.java b/android/app/src/main/java/seven/lab/wstun/marketplace/InstalledService.java new file mode 100644 index 0000000..e711b5e --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/marketplace/InstalledService.java @@ -0,0 +1,122 @@ +package seven.lab.wstun.marketplace; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Represents an installed service with its manifest and files. + */ +public class InstalledService { + + private static final Gson gson = new Gson(); + + @SerializedName("manifest") + private ServiceManifest manifest; + + @SerializedName("enabled") + private boolean enabled; + + @SerializedName("installedAt") + private long installedAt; + + @SerializedName("source") + private String source; // marketplace URL or "builtin" + + // Runtime state (not persisted) + private transient Map files = new ConcurrentHashMap<>(); // path -> content + private transient boolean running; + + public InstalledService() { + this.enabled = false; + this.running = false; + this.installedAt = System.currentTimeMillis(); + } + + public InstalledService(ServiceManifest manifest, String source) { + this(); + this.manifest = manifest; + this.source = source; + } + + // Getters and setters + public ServiceManifest getManifest() { return manifest; } + public void setManifest(ServiceManifest manifest) { this.manifest = manifest; } + + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public long getInstalledAt() { return installedAt; } + public void setInstalledAt(long installedAt) { this.installedAt = installedAt; } + + public String getSource() { return source; } + public void setSource(String source) { this.source = source; } + + public Map getFiles() { return files; } + public void setFiles(Map files) { this.files = files; } + + public boolean isRunning() { return running; } + public void setRunning(boolean running) { this.running = running; } + + /** + * Get file content for an endpoint path. + */ + public String getFileContent(String path) { + if (manifest == null || manifest.getEndpoints() == null) { + return null; + } + + for (ServiceManifest.Endpoint ep : manifest.getEndpoints()) { + if (path.equals(ep.getPath())) { + return files.get(ep.getFile()); + } + } + return null; + } + + /** + * Get service name. + */ + public String getName() { + return manifest != null ? manifest.getName() : null; + } + + /** + * Get display name. + */ + public String getDisplayName() { + return manifest != null ? manifest.getDisplayName() : null; + } + + /** + * Convert to JSON for API. + */ + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + if (manifest != null) { + obj.add("manifest", manifest.toJson()); + } + obj.addProperty("enabled", enabled); + obj.addProperty("running", running); + obj.addProperty("installedAt", installedAt); + obj.addProperty("source", source); + return obj; + } + + /** + * Convert to JSON string for persistence. + */ + public String toJsonString() { + return gson.toJson(this); + } + + /** + * Parse from JSON string. + */ + public static InstalledService fromJson(String json) { + return gson.fromJson(json, InstalledService.class); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/marketplace/MarketplaceService.java b/android/app/src/main/java/seven/lab/wstun/marketplace/MarketplaceService.java new file mode 100644 index 0000000..cd17294 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/marketplace/MarketplaceService.java @@ -0,0 +1,503 @@ +package seven.lab.wstun.marketplace; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Manages marketplace operations: browsing, downloading, installing services. + * + * Marketplace API: + * - GET {baseUrl}/list - Returns JSON array of available services with name, description + * - GET {baseUrl}/service/{name}.json - Returns service manifest + * - GET {baseUrl}/service/{name}/{file} - Downloads service file + */ +public class MarketplaceService { + + private static final String TAG = "MarketplaceService"; + private static final String PREFS_NAME = "wstun_marketplace"; + private static final String KEY_INSTALLED_SERVICES = "installed_services"; + private static final String KEY_MARKETPLACE_URL = "marketplace_url"; + private static final Gson gson = new Gson(); + + private final Context context; + private final File servicesDir; + private final SharedPreferences prefs; + private final ExecutorService executor; + + // Installed services cache + private final Map installedServices = new ConcurrentHashMap<>(); + + // Callback interfaces + public interface MarketplaceCallback { + void onSuccess(T result); + void onError(String error); + } + + public MarketplaceService(Context context) { + this.context = context; + this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + this.executor = Executors.newCachedThreadPool(); + + // Create services directory + this.servicesDir = new File(context.getFilesDir(), "services"); + if (!servicesDir.exists()) { + servicesDir.mkdirs(); + } + + // Load installed services + loadInstalledServices(); + + // Load built-in services + loadBuiltinServices(); + } + + /** + * Load installed services from storage. + */ + private void loadInstalledServices() { + String json = prefs.getString(KEY_INSTALLED_SERVICES, "{}"); + try { + Type type = new TypeToken>(){}.getType(); + Map loaded = gson.fromJson(json, type); + if (loaded != null) { + installedServices.putAll(loaded); + } + + // Load file contents from disk + for (InstalledService service : installedServices.values()) { + loadServiceFiles(service); + } + + Log.i(TAG, "Loaded " + installedServices.size() + " installed services"); + } catch (Exception e) { + Log.e(TAG, "Failed to load installed services", e); + } + } + + /** + * Load built-in services (fileshare, chat) as installed services. + */ + private void loadBuiltinServices() { + // Load fileshare + if (!installedServices.containsKey("fileshare")) { + InstalledService fileshare = createBuiltinService("fileshare", "FileShare", + "Share files with others in real-time"); + installedServices.put("fileshare", fileshare); + } + + // Load chat + if (!installedServices.containsKey("chat")) { + InstalledService chat = createBuiltinService("chat", "Chat", + "Real-time chat rooms"); + installedServices.put("chat", chat); + } + + // Load built-in HTML from assets + loadBuiltinAssets(); + } + + /** + * Create a built-in service entry. + */ + private InstalledService createBuiltinService(String name, String displayName, String description) { + ServiceManifest manifest = new ServiceManifest(); + manifest.setName(name); + manifest.setDisplayName(displayName); + manifest.setDescription(description); + manifest.setVersion("1.0.0"); + manifest.setAuthor("Built-in"); + + List endpoints = new ArrayList<>(); + + ServiceManifest.Endpoint serviceEp = new ServiceManifest.Endpoint(); + serviceEp.setPath("/service"); + serviceEp.setFile("index.html"); + serviceEp.setType("service"); + endpoints.add(serviceEp); + + ServiceManifest.Endpoint mainEp = new ServiceManifest.Endpoint(); + mainEp.setPath("/main"); + mainEp.setFile("main.html"); + mainEp.setType("client"); + endpoints.add(mainEp); + + manifest.setEndpoints(endpoints); + + InstalledService service = new InstalledService(manifest, "builtin"); + service.setInstalledAt(0); // Built-in + service.setEnabled(false); // Disabled by default - user enables on the fly + return service; + } + + /** + * Load built-in HTML assets. + */ + private void loadBuiltinAssets() { + try { + InstalledService fileshare = installedServices.get("fileshare"); + if (fileshare != null && fileshare.getFiles().isEmpty()) { + fileshare.getFiles().put("index.html", loadAsset("services/fileshare/index.html")); + fileshare.getFiles().put("main.html", loadAsset("services/fileshare/main.html")); + } + + InstalledService chat = installedServices.get("chat"); + if (chat != null && chat.getFiles().isEmpty()) { + chat.getFiles().put("index.html", loadAsset("services/chat/index.html")); + chat.getFiles().put("main.html", loadAsset("services/chat/main.html")); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load built-in assets", e); + } + } + + private String loadAsset(String path) throws IOException { + InputStream is = context.getAssets().open(path); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } + + /** + * Save installed services to storage. + */ + private void saveInstalledServices() { + try { + // Create a copy without file contents for storage + Map toSave = new ConcurrentHashMap<>(); + for (Map.Entry entry : installedServices.entrySet()) { + if (!"builtin".equals(entry.getValue().getSource())) { + toSave.put(entry.getKey(), entry.getValue()); + } + } + String json = gson.toJson(toSave); + prefs.edit().putString(KEY_INSTALLED_SERVICES, json).apply(); + } catch (Exception e) { + Log.e(TAG, "Failed to save installed services", e); + } + } + + /** + * Load service files from disk. + */ + private void loadServiceFiles(InstalledService service) { + if (service == null || service.getManifest() == null) return; + if ("builtin".equals(service.getSource())) return; // Built-ins loaded from assets + + File serviceDir = new File(servicesDir, service.getName()); + if (!serviceDir.exists()) return; + + List endpoints = service.getManifest().getEndpoints(); + if (endpoints == null) return; + + for (ServiceManifest.Endpoint ep : endpoints) { + File file = new File(serviceDir, ep.getFile()); + if (file.exists()) { + try { + String content = readFile(file); + service.getFiles().put(ep.getFile(), content); + } catch (IOException e) { + Log.e(TAG, "Failed to load file: " + file.getPath(), e); + } + } + } + } + + private String readFile(File file) throws IOException { + FileInputStream fis = new FileInputStream(file); + BufferedReader reader = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } + + /** + * Get or set marketplace URL. + */ + public String getMarketplaceUrl() { + return prefs.getString(KEY_MARKETPLACE_URL, ""); + } + + public void setMarketplaceUrl(String url) { + prefs.edit().putString(KEY_MARKETPLACE_URL, url).apply(); + } + + /** + * List services from marketplace. + */ + public void listMarketplace(String baseUrl, MarketplaceCallback> callback) { + executor.execute(() -> { + try { + String listUrl = baseUrl.endsWith("/") ? baseUrl + "list" : baseUrl + "/list"; + String json = httpGet(listUrl); + JsonArray arr = gson.fromJson(json, JsonArray.class); + + List services = new ArrayList<>(); + for (JsonElement el : arr) { + services.add(el.getAsJsonObject()); + } + + callback.onSuccess(services); + } catch (Exception e) { + Log.e(TAG, "Failed to list marketplace", e); + callback.onError("Failed to fetch marketplace: " + e.getMessage()); + } + }); + } + + /** + * Get service manifest from marketplace. + */ + public void getServiceManifest(String baseUrl, String serviceName, + MarketplaceCallback callback) { + executor.execute(() -> { + try { + String manifestUrl = baseUrl.endsWith("/") + ? baseUrl + "service/" + serviceName + ".json" + : baseUrl + "/service/" + serviceName + ".json"; + String json = httpGet(manifestUrl); + ServiceManifest manifest = ServiceManifest.fromJson(json); + callback.onSuccess(manifest); + } catch (Exception e) { + Log.e(TAG, "Failed to get manifest for: " + serviceName, e); + callback.onError("Failed to fetch manifest: " + e.getMessage()); + } + }); + } + + /** + * Install a service from marketplace. + */ + public void installService(String baseUrl, String serviceName, + MarketplaceCallback callback) { + executor.execute(() -> { + try { + // Get manifest first + String manifestUrl = baseUrl.endsWith("/") + ? baseUrl + "service/" + serviceName + ".json" + : baseUrl + "/service/" + serviceName + ".json"; + String manifestJson = httpGet(manifestUrl); + ServiceManifest manifest = ServiceManifest.fromJson(manifestJson); + + if (manifest == null || manifest.getName() == null) { + callback.onError("Invalid manifest"); + return; + } + + // Create service directory + File serviceDir = new File(servicesDir, manifest.getName()); + if (!serviceDir.exists()) { + serviceDir.mkdirs(); + } + + // Download all endpoint files + InstalledService service = new InstalledService(manifest, baseUrl); + List endpoints = manifest.getEndpoints(); + + if (endpoints != null) { + for (ServiceManifest.Endpoint ep : endpoints) { + String fileUrl = baseUrl.endsWith("/") + ? baseUrl + "service/" + serviceName + "/" + ep.getFile() + : baseUrl + "/service/" + serviceName + "/" + ep.getFile(); + String content = httpGet(fileUrl); + + // Save to disk + File file = new File(serviceDir, ep.getFile()); + writeFile(file, content); + + // Cache in memory + service.getFiles().put(ep.getFile(), content); + } + } + + // Save manifest to disk + File manifestFile = new File(serviceDir, "manifest.json"); + writeFile(manifestFile, manifestJson); + + // Add to installed services + installedServices.put(manifest.getName(), service); + saveInstalledServices(); + + Log.i(TAG, "Installed service: " + manifest.getName()); + callback.onSuccess(service); + + } catch (Exception e) { + Log.e(TAG, "Failed to install service: " + serviceName, e); + callback.onError("Failed to install: " + e.getMessage()); + } + }); + } + + private void writeFile(File file, String content) throws IOException { + FileOutputStream fos = new FileOutputStream(file); + OutputStreamWriter writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8); + writer.write(content); + writer.close(); + } + + /** + * Uninstall a service. + */ + public boolean uninstallService(String serviceName) { + InstalledService service = installedServices.get(serviceName); + if (service == null) { + return false; + } + + // Don't allow uninstalling built-in services + if ("builtin".equals(service.getSource())) { + Log.w(TAG, "Cannot uninstall built-in service: " + serviceName); + return false; + } + + // Delete service directory + File serviceDir = new File(servicesDir, serviceName); + if (serviceDir.exists()) { + deleteDirectory(serviceDir); + } + + // Remove from cache + installedServices.remove(serviceName); + saveInstalledServices(); + + Log.i(TAG, "Uninstalled service: " + serviceName); + return true; + } + + private void deleteDirectory(File dir) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + dir.delete(); + } + + /** + * Enable a service (attach endpoints). + */ + public boolean enableService(String serviceName) { + InstalledService service = installedServices.get(serviceName); + if (service == null) { + return false; + } + + service.setEnabled(true); + saveInstalledServices(); + Log.i(TAG, "Enabled service: " + serviceName); + return true; + } + + /** + * Disable a service (detach endpoints, close instances). + */ + public boolean disableService(String serviceName) { + InstalledService service = installedServices.get(serviceName); + if (service == null) { + return false; + } + + service.setEnabled(false); + service.setRunning(false); + saveInstalledServices(); + Log.i(TAG, "Disabled service: " + serviceName); + return true; + } + + /** + * Get all installed services. + */ + public Map getInstalledServices() { + return installedServices; + } + + /** + * Get installed service by name. + */ + public InstalledService getInstalledService(String name) { + return installedServices.get(name); + } + + /** + * Check if a service is installed. + */ + public boolean isInstalled(String name) { + return installedServices.containsKey(name); + } + + /** + * HTTP GET request. + */ + private String httpGet(String urlStr) throws IOException { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + + try { + int responseCode = conn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new IOException("HTTP " + responseCode); + } + + BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } finally { + conn.disconnect(); + } + } + + /** + * Shutdown executor. + */ + public void shutdown() { + executor.shutdown(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/marketplace/ServiceManifest.java b/android/app/src/main/java/seven/lab/wstun/marketplace/ServiceManifest.java new file mode 100644 index 0000000..d0b365b --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/marketplace/ServiceManifest.java @@ -0,0 +1,150 @@ +package seven.lab.wstun.marketplace; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +import java.util.List; +import java.util.Map; + +/** + * Represents a service manifest downloaded from marketplace. + * The manifest defines service metadata and files to be downloaded. + * + * Example manifest JSON: + * { + * "name": "test", + * "displayName": "Test Service", + * "description": "A test service for demonstration", + * "version": "1.0.0", + * "author": "Example Author", + * "endpoints": [ + * {"path": "/service", "file": "index.html", "type": "service"}, + * {"path": "/main", "file": "main.html", "type": "client"} + * ] + * } + */ +public class ServiceManifest { + + private static final Gson gson = new Gson(); + + @SerializedName("name") + private String name; + + @SerializedName("displayName") + private String displayName; + + @SerializedName("description") + private String description; + + @SerializedName("version") + private String version; + + @SerializedName("author") + private String author; + + @SerializedName("icon") + private String icon; + + @SerializedName("endpoints") + private List endpoints; + + /** + * Represents an endpoint defined by the service. + */ + public static class Endpoint { + @SerializedName("path") + private String path; // e.g., "/service", "/main" + + @SerializedName("file") + private String file; // e.g., "index.html", "main.html" + + @SerializedName("type") + private String type; // "service" or "client" + + @SerializedName("contentType") + private String contentType; // optional, defaults based on file extension + + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + + public String getFile() { return file; } + public void setFile(String file) { this.file = file; } + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + + public String getContentType() { return contentType; } + public void setContentType(String contentType) { this.contentType = contentType; } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("path", path); + obj.addProperty("file", file); + obj.addProperty("type", type); + if (contentType != null) { + obj.addProperty("contentType", contentType); + } + return obj; + } + } + + // Getters and setters + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getDisplayName() { return displayName != null ? displayName : name; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getVersion() { return version; } + public void setVersion(String version) { this.version = version; } + + public String getAuthor() { return author; } + public void setAuthor(String author) { this.author = author; } + + public String getIcon() { return icon; } + public void setIcon(String icon) { this.icon = icon; } + + public List getEndpoints() { return endpoints; } + public void setEndpoints(List endpoints) { this.endpoints = endpoints; } + + /** + * Parse manifest from JSON string. + */ + public static ServiceManifest fromJson(String json) { + return gson.fromJson(json, ServiceManifest.class); + } + + /** + * Convert to JSON string. + */ + public String toJsonString() { + return gson.toJson(this); + } + + /** + * Convert to JsonObject for API responses. + */ + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("name", name); + obj.addProperty("displayName", getDisplayName()); + obj.addProperty("description", description); + obj.addProperty("version", version); + obj.addProperty("author", author); + if (icon != null) { + obj.addProperty("icon", icon); + } + if (endpoints != null) { + com.google.gson.JsonArray arr = new com.google.gson.JsonArray(); + for (Endpoint ep : endpoints) { + arr.add(ep.toJson()); + } + obj.add("endpoints", arr); + } + return obj; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayRequest.java b/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayRequest.java new file mode 100644 index 0000000..7197f2d --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayRequest.java @@ -0,0 +1,88 @@ +package seven.lab.wstun.protocol; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +/** + * HTTP request relayed to a service client. + */ +public class HttpRelayRequest { + + @SerializedName("request_id") + private String requestId; + + @SerializedName("method") + private String method; + + @SerializedName("path") + private String path; + + @SerializedName("query") + private String query; + + @SerializedName("headers") + private Map headers; + + @SerializedName("body") + private String body; + + @SerializedName("body_base64") + private String bodyBase64; // For binary data + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getBodyBase64() { + return bodyBase64; + } + + public void setBodyBase64(String bodyBase64) { + this.bodyBase64 = bodyBase64; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayResponse.java b/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayResponse.java new file mode 100644 index 0000000..8b18582 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayResponse.java @@ -0,0 +1,99 @@ +package seven.lab.wstun.protocol; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +/** + * HTTP response from a service client. + */ +public class HttpRelayResponse { + + @SerializedName("request_id") + private String requestId; + + @SerializedName("status") + private int status; + + @SerializedName("headers") + private Map headers; + + @SerializedName("body") + private String body; + + @SerializedName("body_base64") + private String bodyBase64; // For binary data + + @SerializedName("streaming") + private boolean streaming; + + @SerializedName("chunk_index") + private int chunkIndex; + + @SerializedName("is_final") + private boolean isFinal; + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getBodyBase64() { + return bodyBase64; + } + + public void setBodyBase64(String bodyBase64) { + this.bodyBase64 = bodyBase64; + } + + public boolean isStreaming() { + return streaming; + } + + public void setStreaming(boolean streaming) { + this.streaming = streaming; + } + + public int getChunkIndex() { + return chunkIndex; + } + + public void setChunkIndex(int chunkIndex) { + this.chunkIndex = chunkIndex; + } + + public boolean isFinal() { + return isFinal; + } + + public void setFinal(boolean aFinal) { + isFinal = aFinal; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/protocol/Message.java b/android/app/src/main/java/seven/lab/wstun/protocol/Message.java new file mode 100644 index 0000000..dcb5726 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/protocol/Message.java @@ -0,0 +1,155 @@ +package seven.lab.wstun.protocol; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +/** + * Base message class for WebSocket communication. + * Supports both text (JSON) and binary modes. + */ +public class Message { + + private static final Gson gson = new Gson(); + + // Message types + public static final String TYPE_REGISTER = "register"; + public static final String TYPE_UNREGISTER = "unregister"; + public static final String TYPE_DATA = "data"; + public static final String TYPE_FILE_REQUEST = "file_request"; + public static final String TYPE_FILE_DATA = "file_data"; + public static final String TYPE_FILE_END = "file_end"; + public static final String TYPE_CHAT_MESSAGE = "chat_message"; + public static final String TYPE_CHAT_JOIN = "chat_join"; + public static final String TYPE_CHAT_LEAVE = "chat_leave"; + public static final String TYPE_ERROR = "error"; + public static final String TYPE_ACK = "ack"; + public static final String TYPE_RELAY_REGISTER = "relay_register"; + public static final String TYPE_RELAY_REQUEST = "relay_request"; + public static final String TYPE_HTTP_REQUEST = "http_request"; + public static final String TYPE_HTTP_RESPONSE = "http_response"; + + // Streaming response types for large file transfers + public static final String TYPE_HTTP_RESPONSE_START = "http_response_start"; + public static final String TYPE_HTTP_RESPONSE_CHUNK = "http_response_chunk"; + + // File registry types for relay file sharing + public static final String TYPE_FILE_REGISTER = "file_register"; + public static final String TYPE_FILE_UNREGISTER = "file_unregister"; + public static final String TYPE_FILE_LIST = "file_list"; + + // Client registration (for user clients, not services) + public static final String TYPE_CLIENT_REGISTER = "client_register"; + + // Chat broadcast types + public static final String TYPE_CHAT_USER_LIST = "chat_user_list"; + + // Service management + public static final String TYPE_KICK_CLIENT = "kick_client"; + + // Instance management + public static final String TYPE_CREATE_INSTANCE = "create_instance"; + public static final String TYPE_JOIN_INSTANCE = "join_instance"; + public static final String TYPE_LEAVE_INSTANCE = "leave_instance"; + public static final String TYPE_LIST_INSTANCES = "list_instances"; + public static final String TYPE_INSTANCE_LIST = "instance_list"; + public static final String TYPE_INSTANCE_CREATED = "instance_created"; + public static final String TYPE_RECLAIM_INSTANCE = "reclaim_instance"; + public static final String TYPE_INSTANCE_RECLAIMED = "instance_reclaimed"; + + // Instance-scoped messages (for fileshare/chat within a room) + public static final String TYPE_USER_JOIN = "user_join"; + public static final String TYPE_USER_LEAVE = "user_leave"; + public static final String TYPE_FILE_ADD = "file_add"; + public static final String TYPE_FILE_REMOVE = "file_remove"; + public static final String TYPE_REQUEST_STATE = "request_state"; + public static final String TYPE_STATE_RESPONSE = "state_response"; + + @SerializedName("type") + private String type; + + @SerializedName("id") + private String id; + + @SerializedName("service") + private String service; + + @SerializedName("payload") + private JsonObject payload; + + @SerializedName("binary") + private boolean binary; + + public Message() { + } + + public Message(String type) { + this.type = type; + this.id = generateId(); + } + + public Message(String type, String service) { + this(type); + this.service = service; + } + + private String generateId() { + return String.valueOf(System.currentTimeMillis()) + "-" + (int)(Math.random() * 10000); + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getService() { + return service; + } + + public void setService(String service) { + this.service = service; + } + + public JsonObject getPayload() { + return payload; + } + + public void setPayload(JsonObject payload) { + this.payload = payload; + } + + public boolean isBinary() { + return binary; + } + + public void setBinary(boolean binary) { + this.binary = binary; + } + + public String toJson() { + return gson.toJson(this); + } + + public static Message fromJson(String json) { + return gson.fromJson(json, Message.class); + } + + public static T payloadAs(JsonObject payload, Class clazz) { + return gson.fromJson(payload, clazz); + } + + public static JsonObject toPayload(Object obj) { + return gson.toJsonTree(obj).getAsJsonObject(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/protocol/ServiceRegistration.java b/android/app/src/main/java/seven/lab/wstun/protocol/ServiceRegistration.java new file mode 100644 index 0000000..8d78202 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/protocol/ServiceRegistration.java @@ -0,0 +1,124 @@ +package seven.lab.wstun.protocol; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; +import java.util.Map; + +/** + * Service registration data sent by clients. + */ +public class ServiceRegistration { + + @SerializedName("name") + private String name; + + @SerializedName("type") + private String type; + + @SerializedName("description") + private String description; + + @SerializedName("endpoints") + private List endpoints; + + @SerializedName("static_resources") + private Map staticResources; // path -> content + + @SerializedName("auth_token") + private String authToken; // Optional auth token for clients to access this service + + public static class Endpoint { + @SerializedName("path") + private String path; + + @SerializedName("method") + private String method; // GET, POST, etc. + + @SerializedName("description") + private String description; + + @SerializedName("relay") + private boolean relay; // Whether to relay requests to the client + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isRelay() { + return relay; + } + + public void setRelay(boolean relay) { + this.relay = relay; + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getEndpoints() { + return endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + + public Map getStaticResources() { + return staticResources; + } + + public void setStaticResources(Map staticResources) { + this.staticResources = staticResources; + } + + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/HttpHandler.java b/android/app/src/main/java/seven/lab/wstun/server/HttpHandler.java new file mode 100644 index 0000000..4d08699 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/HttpHandler.java @@ -0,0 +1,1448 @@ +package seven.lab.wstun.server; + +import android.util.Base64; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.netty.handler.codec.http.DefaultHttpContent; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; +import seven.lab.wstun.config.ServerConfig; +import seven.lab.wstun.protocol.HttpRelayRequest; +import seven.lab.wstun.protocol.HttpRelayResponse; +import seven.lab.wstun.protocol.Message; + +/** + * HTTP request handler that routes requests to registered services. + */ +public class HttpHandler extends SimpleChannelInboundHandler { + + private static final String TAG = "HttpHandler"; + private static final Gson gson = new Gson(); + + private final ServiceManager serviceManager; + private final RequestManager requestManager; + private final LocalServiceManager localServiceManager; + private final boolean ssl; + private final String corsOrigins; + private final int port; + private final ServerConfig serverConfig; + + private WebSocketServerHandshaker handshaker; + + // Static reference for relay responses + private static String staticCorsOrigins = "*"; + + // Static reference for server config (for WebSocket handler) + private static ServerConfig staticServerConfig; + + // Track channels with active streaming responses + private static final Map streamingResponses = new ConcurrentHashMap<>(); + + public HttpHandler(ServiceManager serviceManager, RequestManager requestManager, + LocalServiceManager localServiceManager, boolean ssl, String corsOrigins, int port, + ServerConfig serverConfig) { + this.serviceManager = serviceManager; + this.requestManager = requestManager; + this.localServiceManager = localServiceManager; + this.ssl = ssl; + this.corsOrigins = corsOrigins != null ? corsOrigins : "*"; + this.port = port; + this.serverConfig = serverConfig; + staticCorsOrigins = this.corsOrigins; + staticServerConfig = serverConfig; + } + + public HttpHandler(ServiceManager serviceManager, RequestManager requestManager, boolean ssl) { + this(serviceManager, requestManager, null, ssl, "*", 8080, null); + } + + public static ServerConfig getServerConfig() { + return staticServerConfig; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + Log.d(TAG, "HTTP Request: " + request.method() + " " + request.uri()); + try { + // Check for WebSocket upgrade + if (isWebSocketUpgrade(request)) { + handleWebSocketUpgrade(ctx, request); + return; + } + + // Handle HTTP request + handleHttpRequest(ctx, request); + } catch (Exception e) { + Log.e(TAG, "Error handling HTTP request", e); + try { + sendInternalServerError(ctx, request, e.getMessage()); + } catch (Exception e2) { + Log.e(TAG, "Error sending error response", e2); + ctx.close(); + } + } + } + + private boolean isWebSocketUpgrade(FullHttpRequest request) { + String upgrade = request.headers().get(HttpHeaderNames.UPGRADE); + return "websocket".equalsIgnoreCase(upgrade); + } + + private void handleWebSocketUpgrade(ChannelHandlerContext ctx, FullHttpRequest request) { + String wsUrl = (ssl ? "wss" : "ws") + "://" + request.headers().get(HttpHeaderNames.HOST) + "/ws"; + + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + wsUrl, null, true, 65536 + ); + + handshaker = wsFactory.newHandshaker(request); + if (handshaker == null) { + WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); + } else { + handshaker.handshake(ctx.channel(), request).addListener(future -> { + if (future.isSuccess()) { + // Replace this handler with WebSocket handler + ctx.pipeline().replace(this, "websocket", + new WebSocketHandler(serviceManager, requestManager)); + Log.i(TAG, "WebSocket connection established"); + } + }); + } + } + + private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) { + String uri = request.uri(); + QueryStringDecoder decoder = new QueryStringDecoder(uri); + String path = decoder.path(); + + // Only log at INFO level to reduce overhead + // Log.d(TAG, "HTTP " + request.method() + " " + path); + + // Handle CORS preflight + if (request.method() == HttpMethod.OPTIONS) { + sendCorsPreflightResponse(ctx, request); + return; + } + + // Check server auth (skip for libwstun.js which is public) + if (!"/libwstun.js".equals(path) && !validateServerAuth(request)) { + sendUnauthorized(ctx, request, "Invalid or missing server auth token"); + return; + } + + // Root path - show server info + if ("/".equals(path)) { + sendServerInfo(ctx, request); + return; + } + + // Admin/management page + if ("/admin".equals(path) || "/admin/".equals(path)) { + sendAdminPage(ctx, request); + return; + } + + // Debug logs endpoint - streams logcat (only if enabled in config) + if ("/debug/logs".equals(path)) { + if (serverConfig != null && serverConfig.isDebugLogsEnabled()) { + handleDebugLogs(ctx, request); + } else { + sendNotFound(ctx, request); + } + return; + } + + // Serve libwstun.js library (public, no auth) + if ("/libwstun.js".equals(path)) { + sendLibWstun(ctx, request); + return; + } + + // Parse service name from path + String[] pathParts = path.split("/"); + if (pathParts.length < 2) { + sendNotFound(ctx, request); + return; + } + + String serviceName = pathParts[1]; + + // Handle marketplace and service management API + if ("_api".equals(serviceName)) { + handleApiRequest(ctx, request, pathParts); + return; + } + + // Check for local/installed service endpoints + if (localServiceManager != null) { + seven.lab.wstun.marketplace.InstalledService installedSvc = localServiceManager.getInstalledService(serviceName); + if (installedSvc != null) { + if (handleLocalService(ctx, request, serviceName, pathParts)) { + return; + } + } + } + + // Check for file download requests (/fileshare/download/{fileId}) + if ("fileshare".equals(serviceName) && pathParts.length >= 4 && "download".equals(pathParts[2])) { + handleFileDownload(ctx, request, pathParts); + return; + } + + ServiceManager.ServiceEntry service = serviceManager.getService(serviceName); + + if (service == null) { + sendNotFound(ctx, request); + return; + } + + // Check for static resources + if (service.getRegistration().getStaticResources() != null) { + String resourcePath = path.substring(serviceName.length() + 1); + String content = service.getRegistration().getStaticResources().get(resourcePath); + if (content != null) { + sendStaticResource(ctx, request, content, resourcePath); + return; + } + } + + // Relay request to service client + relayRequest(ctx, request, service, path); + } + + private void relayRequest(ChannelHandlerContext ctx, FullHttpRequest request, + ServiceManager.ServiceEntry service, String path) { + // Create relay request + String requestId = String.valueOf(System.currentTimeMillis()) + "-" + (int)(Math.random() * 10000); + + HttpRelayRequest relayRequest = new HttpRelayRequest(); + relayRequest.setRequestId(requestId); + relayRequest.setMethod(request.method().name()); + relayRequest.setPath(path); + + QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); + relayRequest.setQuery(request.uri().contains("?") ? + request.uri().substring(request.uri().indexOf("?") + 1) : ""); + + // Copy headers + Map headers = new HashMap<>(); + for (Map.Entry entry : request.headers()) { + headers.put(entry.getKey(), entry.getValue()); + } + relayRequest.setHeaders(headers); + + // Copy body + ByteBuf content = request.content(); + if (content.readableBytes() > 0) { + byte[] bodyBytes = new byte[content.readableBytes()]; + content.readBytes(bodyBytes); + + // Check if binary + String contentType = request.headers().get(HttpHeaderNames.CONTENT_TYPE); + if (contentType != null && isBinaryContentType(contentType)) { + relayRequest.setBodyBase64(Base64.encodeToString(bodyBytes, Base64.NO_WRAP)); + } else { + relayRequest.setBody(new String(bodyBytes, StandardCharsets.UTF_8)); + } + } + + // Store pending request + PendingRequest pending = new PendingRequest(requestId, ctx, request, service.getName()); + requestManager.addPendingRequest(pending); + + // Send to service via WebSocket + Message message = new Message(Message.TYPE_HTTP_REQUEST, service.getName()); + message.setPayload(Message.toPayload(relayRequest)); + + if (service.getChannel() != null && service.getChannel().isActive()) { + service.getChannel().writeAndFlush(new TextWebSocketFrame(message.toJson())); + } else { + requestManager.removePendingRequest(requestId); + sendServiceUnavailable(ctx, request); + } + } + + private boolean isBinaryContentType(String contentType) { + return contentType.startsWith("application/octet-stream") || + contentType.startsWith("image/") || + contentType.startsWith("audio/") || + contentType.startsWith("video/"); + } + + /** + * Send relay response to HTTP client. + */ + public static void sendRelayResponse(ChannelHandlerContext ctx, HttpRelayResponse response) { + HttpResponseStatus status = HttpResponseStatus.valueOf(response.getStatus()); + + byte[] body; + if (response.getBodyBase64() != null) { + body = Base64.decode(response.getBodyBase64(), Base64.NO_WRAP); + } else if (response.getBody() != null) { + body = response.getBody().getBytes(StandardCharsets.UTF_8); + } else { + body = new byte[0]; + } + + FullHttpResponse httpResponse = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + status, + Unpooled.wrappedBuffer(body) + ); + + // Add CORS headers using static method + addStaticCorsHeaders(httpResponse); + + // Copy headers + if (response.getHeaders() != null) { + for (Map.Entry entry : response.getHeaders().entrySet()) { + httpResponse.headers().set(entry.getKey(), entry.getValue()); + } + } + + httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, body.length); + httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + + ctx.writeAndFlush(httpResponse).addListener(ChannelFutureListener.CLOSE); + } + + /** + * Handle local service endpoints (/fileshare/service, /chat/service, etc.) + * Returns true if the request was handled. + */ + private boolean handleLocalService(ChannelHandlerContext ctx, FullHttpRequest request, + String serviceName, String[] pathParts) { + String subPath = pathParts.length > 2 ? pathParts[2] : ""; + + // Build server URL for generating links + String host = request.headers().get(HttpHeaderNames.HOST); + if (host == null) { + host = "localhost:" + port; + } + String protocol = ssl ? "https" : "http"; + String serverUrl = protocol + "://" + host; + + // /service - Service management page + if ("service".equals(subPath)) { + // Check for API endpoints under /service/api/* + if (pathParts.length > 3 && "api".equals(pathParts[3])) { + return handleLocalServiceApi(ctx, request, serviceName, pathParts); + } + + // Service management page + String html = localServiceManager.getServicePageHtml(serviceName, serverUrl); + if (html == null) { + sendNotFound(ctx, request); + } else { + sendHtmlResponse(ctx, request, html); + } + return true; + } + + // /main - Main service UI (serve directly when service is registered) + if ("main".equals(subPath)) { + // Serve the user client HTML directly from assets (regardless of service running state) + String html = localServiceManager.getServiceMainHtml(serviceName); + if (html != null) { + sendHtmlResponse(ctx, request, html); + return true; + } + + // Fallback: send not found + sendNotFound(ctx, request); + return true; + } + + return false; + } + + /** + * Handle local service API endpoints. + */ + private boolean handleLocalServiceApi(ChannelHandlerContext ctx, FullHttpRequest request, + String serviceName, String[] pathParts) { + if (pathParts.length < 5) { + return false; + } + + String apiAction = pathParts[4]; + + // GET /service/api/status - Get service status + if ("status".equals(apiAction) && request.method() == HttpMethod.GET) { + LocalServiceManager.ServiceStatus status = localServiceManager.getServiceStatus(serviceName); + if (status != null) { + sendJsonResponse(ctx, request, status.toJson().toString()); + } else { + sendJsonResponse(ctx, request, "{\"error\": \"Service not found\"}"); + } + return true; + } + + // POST /service/api/start - Start service + if ("start".equals(apiAction) && request.method() == HttpMethod.POST) { + boolean success = localServiceManager.startService(serviceName); + LocalServiceManager.ServiceStatus status = localServiceManager.getServiceStatus(serviceName); + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (success && status != null) { + response.addProperty("uuid", status.getUuid()); + } else if (!success) { + response.addProperty("error", "Failed to start service"); + } + sendJsonResponse(ctx, request, response.toString()); + return true; + } + + // POST /service/api/stop - Stop service + if ("stop".equals(apiAction) && request.method() == HttpMethod.POST) { + // Parse request body for UUID + String uuid = null; + ByteBuf content = request.content(); + if (content.readableBytes() > 0) { + byte[] bodyBytes = new byte[content.readableBytes()]; + content.readBytes(bodyBytes); + try { + JsonObject body = gson.fromJson(new String(bodyBytes, StandardCharsets.UTF_8), JsonObject.class); + if (body.has("uuid")) { + uuid = body.get("uuid").getAsString(); + } + } catch (Exception e) { + // Ignore parse errors + } + } + + boolean success; + if (uuid != null) { + success = localServiceManager.stopServiceByUuid(serviceName, uuid); + } else { + success = localServiceManager.stopService(serviceName); + } + + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Failed to stop service (UUID mismatch or service not running)"); + } + sendJsonResponse(ctx, request, response.toString()); + return true; + } + + // GET /service/api/clients - List connected clients + if ("clients".equals(apiAction) && request.method() == HttpMethod.GET) { + java.util.List clients = serviceManager.getClientsByType(serviceName); + com.google.gson.JsonArray clientsArray = new com.google.gson.JsonArray(); + for (ServiceManager.ClientInfo client : clients) { + JsonObject clientObj = new JsonObject(); + clientObj.addProperty("userId", client.getUserId()); + clientObj.addProperty("clientType", client.getClientType()); + clientObj.addProperty("connectedAt", client.getConnectedAt()); + clientObj.addProperty("connected", client.getChannel() != null && client.getChannel().isActive()); + clientsArray.add(clientObj); + } + JsonObject response = new JsonObject(); + response.add("clients", clientsArray); + response.addProperty("count", clients.size()); + sendJsonResponse(ctx, request, response.toString()); + return true; + } + + // POST /service/api/kick - Kick a client + if ("kick".equals(apiAction) && request.method() == HttpMethod.POST) { + String userId = null; + ByteBuf content = request.content(); + if (content.readableBytes() > 0) { + byte[] bodyBytes = new byte[content.readableBytes()]; + content.readBytes(bodyBytes); + try { + JsonObject body = gson.fromJson(new String(bodyBytes, StandardCharsets.UTF_8), JsonObject.class); + if (body.has("userId")) { + userId = body.get("userId").getAsString(); + } + } catch (Exception e) { + // Ignore parse errors + } + } + + JsonObject response = new JsonObject(); + if (userId == null || userId.isEmpty()) { + response.addProperty("success", false); + response.addProperty("error", "userId is required"); + } else { + boolean success = serviceManager.kickClient(userId); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Client not found: " + userId); + } + } + sendJsonResponse(ctx, request, response.toString()); + return true; + } + + return false; + } + + /** + * Send response when service is not running. + */ + private void sendServiceNotRunningResponse(ChannelHandlerContext ctx, FullHttpRequest request, + String serviceName, String serverUrl) { + String displayName = "fileshare".equals(serviceName) ? "FileShare" : "Chat"; + String html = "\n" + + "\n" + + "" + displayName + " - Not Running\n" + + "\n" + + "

Service Not Running

\n" + + "

The " + displayName + " service is not currently running.

\n" + + "Start Service
"; + sendHtmlResponse(ctx, request, html); + } + + /** + * Handle API requests (/_api/...). + */ + private void handleApiRequest(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + if (pathParts.length < 3) { + sendJsonResponse(ctx, request, "{\"error\": \"Invalid API path\"}"); + return; + } + + String apiCategory = pathParts[2]; + + // /_api/services - Service management + if ("services".equals(apiCategory)) { + handleServicesApi(ctx, request, pathParts); + return; + } + + // /_api/instances - Instance management + if ("instances".equals(apiCategory)) { + handleInstancesApi(ctx, request, pathParts); + return; + } + + // /_api/marketplace - Marketplace operations + if ("marketplace".equals(apiCategory)) { + handleMarketplaceApi(ctx, request, pathParts); + return; + } + + sendJsonResponse(ctx, request, "{\"error\": \"Unknown API category\"}"); + } + + /** + * Handle /_api/services endpoints. + */ + private void handleServicesApi(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + // GET /_api/services - List all installed services + if (pathParts.length == 3 && request.method() == HttpMethod.GET) { + if (localServiceManager != null) { + JsonObject response = new JsonObject(); + response.add("services", localServiceManager.getInstalledServicesJson()); + sendJsonResponse(ctx, request, response.toString()); + } else { + sendJsonResponse(ctx, request, "{\"services\": []}"); + } + return; + } + + // /_api/services/{name}/... + if (pathParts.length >= 4) { + String serviceName = pathParts[3]; + + // GET /_api/services/{name} - Get service details + if (pathParts.length == 4 && request.method() == HttpMethod.GET) { + seven.lab.wstun.marketplace.InstalledService service = + localServiceManager != null ? localServiceManager.getInstalledService(serviceName) : null; + if (service != null) { + JsonObject obj = service.toJson(); + obj.addProperty("name", serviceName); + obj.addProperty("instanceCount", serviceManager.getInstanceCountForService(serviceName)); + sendJsonResponse(ctx, request, obj.toString()); + } else { + sendJsonResponse(ctx, request, "{\"error\": \"Service not found\"}"); + } + return; + } + + // POST /_api/services/{name}/enable + if (pathParts.length == 5 && "enable".equals(pathParts[4]) && request.method() == HttpMethod.POST) { + boolean success = localServiceManager != null && localServiceManager.enableService(serviceName); + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Failed to enable service"); + } + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // POST /_api/services/{name}/disable + if (pathParts.length == 5 && "disable".equals(pathParts[4]) && request.method() == HttpMethod.POST) { + boolean success = localServiceManager != null && + localServiceManager.disableService(serviceName, serviceManager); + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Failed to disable service"); + } + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // DELETE /_api/services/{name} - Uninstall service + if (pathParts.length == 4 && request.method() == HttpMethod.DELETE) { + boolean success = localServiceManager != null && + localServiceManager.uninstallService(serviceName, serviceManager); + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Failed to uninstall service (may be built-in)"); + } + sendJsonResponse(ctx, request, response.toString()); + return; + } + } + + sendJsonResponse(ctx, request, "{\"error\": \"Invalid services API path\"}"); + } + + /** + * Handle /_api/instances endpoints. + */ + private void handleInstancesApi(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + // GET /_api/instances - List all running instances + if (pathParts.length == 3 && request.method() == HttpMethod.GET) { + com.google.gson.JsonArray instancesArr = new com.google.gson.JsonArray(); + for (ServiceManager.ServiceInstance instance : getAllInstances()) { + instancesArr.add(instance.toJson()); + } + JsonObject response = new JsonObject(); + response.add("instances", instancesArr); + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // GET /_api/instances/{service} - List instances for a service + if (pathParts.length == 4 && request.method() == HttpMethod.GET) { + String serviceName = pathParts[3]; + com.google.gson.JsonArray instancesArr = new com.google.gson.JsonArray(); + for (ServiceManager.ServiceInstance instance : serviceManager.getInstancesForService(serviceName)) { + instancesArr.add(instance.toJson()); + } + JsonObject response = new JsonObject(); + response.add("instances", instancesArr); + sendJsonResponse(ctx, request, response.toString()); + return; + } + + sendJsonResponse(ctx, request, "{\"error\": \"Invalid instances API path\"}"); + } + + /** + * Get all instances across all services. + */ + private java.util.List getAllInstances() { + java.util.List all = new java.util.ArrayList<>(); + if (localServiceManager != null) { + for (String serviceName : localServiceManager.getInstalledServices().keySet()) { + all.addAll(serviceManager.getInstancesForService(serviceName)); + } + } + return all; + } + + /** + * Handle /_api/marketplace endpoints. + */ + private void handleMarketplaceApi(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + if (localServiceManager == null) { + sendJsonResponse(ctx, request, "{\"error\": \"Marketplace not available\"}"); + return; + } + + seven.lab.wstun.marketplace.MarketplaceService marketplace = + localServiceManager.getMarketplaceService(); + + // GET /_api/marketplace/url - Get current marketplace URL + if (pathParts.length == 4 && "url".equals(pathParts[3]) && request.method() == HttpMethod.GET) { + JsonObject response = new JsonObject(); + response.addProperty("url", marketplace.getMarketplaceUrl()); + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // POST /_api/marketplace/url - Set marketplace URL + if (pathParts.length == 4 && "url".equals(pathParts[3]) && request.method() == HttpMethod.POST) { + String url = getRequestBodyString(request, "url"); + if (url != null) { + marketplace.setMarketplaceUrl(url); + } + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.addProperty("url", marketplace.getMarketplaceUrl()); + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // POST /_api/marketplace/list - List services from marketplace + if (pathParts.length == 4 && "list".equals(pathParts[3]) && request.method() == HttpMethod.POST) { + String url = getRequestBodyString(request, "url"); + if (url == null || url.isEmpty()) { + url = marketplace.getMarketplaceUrl(); + } + if (url == null || url.isEmpty()) { + sendJsonResponse(ctx, request, "{\"error\": \"No marketplace URL specified\"}"); + return; + } + + final String marketplaceUrl = url; + marketplace.listMarketplace(marketplaceUrl, + new seven.lab.wstun.marketplace.MarketplaceService.MarketplaceCallback>() { + @Override + public void onSuccess(java.util.List result) { + ctx.executor().execute(() -> { + com.google.gson.JsonArray arr = new com.google.gson.JsonArray(); + for (JsonObject svc : result) { + // Add installed status + String name = svc.has("name") ? svc.get("name").getAsString() : null; + if (name != null) { + svc.addProperty("installed", marketplace.isInstalled(name)); + } + arr.add(svc); + } + JsonObject response = new JsonObject(); + response.add("services", arr); + response.addProperty("url", marketplaceUrl); + sendJsonResponse(ctx, request, response.toString()); + }); + } + + @Override + public void onError(String error) { + ctx.executor().execute(() -> { + JsonObject response = new JsonObject(); + response.addProperty("error", error); + sendJsonResponse(ctx, request, response.toString()); + }); + } + }); + return; + } + + // POST /_api/marketplace/install - Install a service + if (pathParts.length == 4 && "install".equals(pathParts[3]) && request.method() == HttpMethod.POST) { + String url = getRequestBodyString(request, "url"); + String name = getRequestBodyString(request, "name"); + + if (url == null || url.isEmpty()) { + url = marketplace.getMarketplaceUrl(); + } + if (name == null || name.isEmpty()) { + sendJsonResponse(ctx, request, "{\"error\": \"Service name required\"}"); + return; + } + + final String marketplaceUrl = url; + final String serviceName = name; + + marketplace.installService(marketplaceUrl, serviceName, + new seven.lab.wstun.marketplace.MarketplaceService.MarketplaceCallback() { + @Override + public void onSuccess(seven.lab.wstun.marketplace.InstalledService result) { + ctx.executor().execute(() -> { + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.add("service", result.toJson()); + sendJsonResponse(ctx, request, response.toString()); + }); + } + + @Override + public void onError(String error) { + ctx.executor().execute(() -> { + JsonObject response = new JsonObject(); + response.addProperty("success", false); + response.addProperty("error", error); + sendJsonResponse(ctx, request, response.toString()); + }); + } + }); + return; + } + + sendJsonResponse(ctx, request, "{\"error\": \"Invalid marketplace API path\"}"); + } + + /** + * Helper to get a string value from request JSON body. + */ + private String getRequestBodyString(FullHttpRequest request, String key) { + ByteBuf content = request.content(); + if (content.readableBytes() > 0) { + byte[] bodyBytes = new byte[content.readableBytes()]; + content.getBytes(content.readerIndex(), bodyBytes); + try { + JsonObject body = gson.fromJson(new String(bodyBytes, StandardCharsets.UTF_8), JsonObject.class); + if (body.has(key) && !body.get(key).isJsonNull()) { + return body.get(key).getAsString(); + } + } catch (Exception e) { + // Ignore parse errors + } + } + return null; + } + + /** + * Send JSON response. + */ + private void sendJsonResponse(ChannelHandlerContext ctx, FullHttpRequest request, String json) { + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + + sendResponse(ctx, request, response); + } + + private void sendServerInfo(ChannelHandlerContext ctx, FullHttpRequest request) { + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(""); + html.append("WSTun Server"); + html.append(""); + html.append("
"); + html.append("

WSTun Server

"); + html.append("

Service Manager

"); + + // Installed services section + if (localServiceManager != null) { + html.append("

Installed Services

"); + html.append("
    "); + + for (java.util.Map.Entry entry : + localServiceManager.getInstalledServices().entrySet()) { + String name = entry.getKey(); + seven.lab.wstun.marketplace.InstalledService svc = entry.getValue(); + String displayName = svc.getDisplayName(); + boolean enabled = svc.isEnabled(); + int instanceCount = serviceManager.getInstanceCountForService(name); + + html.append("
  • ").append(displayName).append(""); + + if (enabled) { + html.append("Enabled"); + if (instanceCount > 0) { + html.append("").append(instanceCount).append(" instances"); + } + html.append(" - Manage"); + } else { + html.append("Disabled"); + } + + html.append("
  • "); + } + + if (localServiceManager.getInstalledServices().isEmpty()) { + html.append("
  • No services installed
  • "); + } + + html.append("
"); + } + + html.append("

Registered Services

"); + + if (serviceManager.getServiceCount() == 0) { + html.append("

No services registered

"); + } else { + html.append("
    "); + for (ServiceManager.ServiceEntry service : serviceManager.getAllServices()) { + html.append("
  • ").append(service.getName()).append(""); + html.append(" (").append(service.getType()).append(")"); + html.append(" - ").append("/").append(service.getName()).append("/main"); + html.append("
  • "); + } + html.append("
"); + } + + html.append("
"); + + sendHtmlResponse(ctx, request, html.toString()); + } + + private void sendStaticResource(ChannelHandlerContext ctx, FullHttpRequest request, + String content, String path) { + String contentType = "text/plain"; + if (path.endsWith(".html")) { + contentType = "text/html; charset=UTF-8"; + } else if (path.endsWith(".js")) { + contentType = "application/javascript"; + } else if (path.endsWith(".css")) { + contentType = "text/css"; + } else if (path.endsWith(".json")) { + contentType = "application/json"; + } + + byte[] bytes = content.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + + sendResponse(ctx, request, response); + } + + private void sendHtmlResponse(ChannelHandlerContext ctx, FullHttpRequest request, String html) { + if (html == null) { + sendNotFound(ctx, request); + return; + } + byte[] bytes = html.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + + sendResponse(ctx, request, response); + } + + private void sendNotFound(ChannelHandlerContext ctx, FullHttpRequest request) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.NOT_FOUND, + Unpooled.wrappedBuffer("Not Found".getBytes()) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 9); + + sendResponse(ctx, request, response); + } + + private void sendUnauthorized(ChannelHandlerContext ctx, FullHttpRequest request, String message) { + byte[] bytes = message.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.UNAUTHORIZED, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + response.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "Bearer realm=\"wstun\""); + + sendResponse(ctx, request, response); + } + + /** + * Validate server-level authentication. + * Checks Authorization header or ?token query parameter. + */ + private boolean validateServerAuth(FullHttpRequest request) { + if (serverConfig == null || !serverConfig.isAuthEnabled()) { + return true; + } + + String token = null; + + // Check Authorization header (Bearer token) + String authHeader = request.headers().get(HttpHeaderNames.AUTHORIZATION); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + } + + // Check query parameter as fallback + if (token == null) { + QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); + if (decoder.parameters().containsKey("token")) { + token = decoder.parameters().get("token").get(0); + } + } + + return serverConfig.validateServerAuth(token); + } + + /** + * Serve the admin page. + */ + private void sendAdminPage(ChannelHandlerContext ctx, FullHttpRequest request) { + String html = localServiceManager != null ? localServiceManager.getAdminHtml() : null; + if (html == null) { + sendNotFound(ctx, request); + return; + } + sendHtmlResponse(ctx, request, html); + } + + /** + * Serve the libwstun.js library. + */ + private void sendLibWstun(ChannelHandlerContext ctx, FullHttpRequest request) { + String js = localServiceManager != null ? localServiceManager.getLibWstunJs() : null; + if (js == null) { + sendNotFound(ctx, request); + return; + } + + byte[] bytes = js.getBytes(StandardCharsets.UTF_8); + + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/javascript; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "public, max-age=3600"); + + sendResponse(ctx, request, response); + } + + /** + * Handle debug logs endpoint - streams logcat to the browser. + */ + private void handleDebugLogs(ChannelHandlerContext ctx, FullHttpRequest request) { + // Send initial response with chunked transfer encoding + HttpResponse response = new DefaultHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-cache"); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, corsOrigins); + + ctx.writeAndFlush(response); + + // Start a thread to stream logcat + Thread logThread = new Thread(() -> { + Process process = null; + BufferedReader reader = null; + try { + // Start logcat process - filter to show Info and above, with timestamp + process = Runtime.getRuntime().exec("logcat -v time *:I"); + reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + String line; + while (ctx.channel().isActive() && (line = reader.readLine()) != null) { + // Send each log line as a chunk + String chunk = line + "\n"; + ctx.writeAndFlush(new DefaultHttpContent( + Unpooled.copiedBuffer(chunk, StandardCharsets.UTF_8))); + } + } catch (Exception e) { + Log.e(TAG, "Error streaming logcat", e); + } finally { + // Clean up + if (reader != null) { + try { reader.close(); } catch (Exception ignored) {} + } + if (process != null) { + process.destroy(); + } + + // Send end marker and close + if (ctx.channel().isActive()) { + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) + .addListener(ChannelFutureListener.CLOSE); + } + } + }); + logThread.setName("LogcatStreamer"); + logThread.setDaemon(true); + logThread.start(); + } + + private void sendServiceUnavailable(ChannelHandlerContext ctx, FullHttpRequest request) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.SERVICE_UNAVAILABLE, + Unpooled.wrappedBuffer("Service Unavailable".getBytes()) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 19); + + sendResponse(ctx, request, response); + } + + private void sendInternalServerError(ChannelHandlerContext ctx, FullHttpRequest request, String message) { + String body = "Internal Server Error: " + (message != null ? message : "Unknown error"); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.INTERNAL_SERVER_ERROR, + Unpooled.wrappedBuffer(body.getBytes(StandardCharsets.UTF_8)) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, body.getBytes(StandardCharsets.UTF_8).length); + + sendResponse(ctx, request, response); + } + + private void sendResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) { + // Add CORS headers to all responses + addCorsHeaders(response); + + // Ensure Content-Length is set - this is critical for keep-alive to work properly + if (!response.headers().contains(HttpHeaderNames.CONTENT_LENGTH)) { + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + } + + boolean keepAlive = HttpUtil.isKeepAlive(request); + + if (keepAlive) { + // For keep-alive, set the header and don't close + if (!response.headers().contains(HttpHeaderNames.CONNECTION)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + } else { + // For non-keep-alive, set Connection: close + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } + + // Write and flush the response + ctx.writeAndFlush(response).addListener(f -> { + if (!keepAlive) { + ctx.close(); + } + }); + } + + /** + * Add CORS headers to response. + */ + private void addCorsHeaders(FullHttpResponse response) { + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, corsOrigins); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization, X-Requested-With"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, "86400"); + } + + /** + * Add CORS headers with static origin (for relay responses). + */ + private static void addStaticCorsHeaders(FullHttpResponse response) { + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, staticCorsOrigins); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization, X-Requested-With"); + } + + /** + * Handle CORS preflight (OPTIONS) requests. + */ + private void sendCorsPreflightResponse(ChannelHandlerContext ctx, FullHttpRequest request) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.EMPTY_BUFFER + ); + + addCorsHeaders(response); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 0); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + Log.d(TAG, "Channel active: " + ctx.channel().remoteAddress()); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + Log.d(TAG, "Channel inactive: " + ctx.channel().remoteAddress()); + super.channelInactive(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + Log.e(TAG, "HTTP handler error", cause); + ctx.close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof io.netty.handler.timeout.IdleStateEvent) { + Log.d(TAG, "Idle timeout, closing connection"); + ctx.close(); + } else { + super.userEventTriggered(ctx, evt); + } + } + + // ==================== Streaming Response Methods ==================== + + /** + * Start a streaming HTTP response (send headers, prepare for chunks). + */ + public static void startStreamingResponse(ChannelHandlerContext ctx, String requestId, + int status, JsonObject headers) { + HttpResponse response = new DefaultHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.valueOf(status) + ); + + // Add CORS headers + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, staticCorsOrigins); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); + + // Use chunked transfer encoding for streaming + response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + + // Copy headers from payload + if (headers != null) { + for (String key : headers.keySet()) { + // Skip Content-Length as we're using chunked encoding + if (!key.equalsIgnoreCase("Content-Length")) { + response.headers().set(key, headers.get(key).getAsString()); + } + } + } + + // Flush the headers immediately so the client knows the response has started + ctx.writeAndFlush(response); + streamingResponses.put(requestId, ctx); + Log.d(TAG, "Started streaming response for: " + requestId); + } + + /** + * Send a chunk of streaming data. + */ + public static void sendStreamingChunk(ChannelHandlerContext ctx, String chunkBase64) { + if (ctx == null || !ctx.channel().isActive()) { + return; + } + + byte[] data = Base64.decode(chunkBase64, Base64.NO_WRAP); + ByteBuf buf = Unpooled.wrappedBuffer(data); + ctx.writeAndFlush(new io.netty.handler.codec.http.DefaultHttpContent(buf)); + } + + /** + * End a streaming response. + */ + public static void endStreamingResponse(ChannelHandlerContext ctx) { + if (ctx == null || !ctx.channel().isActive()) { + return; + } + + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) + .addListener(ChannelFutureListener.CLOSE); + Log.d(TAG, "Ended streaming response"); + } + + /** + * Send error and close streaming response. + */ + public static void sendStreamingError(ChannelHandlerContext ctx, String error) { + if (ctx == null || !ctx.channel().isActive()) { + return; + } + + // If headers not sent yet, send error response + byte[] errorBytes = error.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.INTERNAL_SERVER_ERROR, + Unpooled.wrappedBuffer(errorBytes) + ); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, errorBytes.length); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + addStaticCorsHeaders(response); + + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + /** + * Request file stream from owner for relay download. + */ + public static void requestFileStream(ServiceManager serviceManager, String requestId, + String fileId, ChannelHandlerContext httpCtx) { + ServiceManager.FileInfo file = serviceManager.getFile(fileId); + if (file == null) { + sendNotFoundResponse(httpCtx); + return; + } + + Channel ownerChannel = file.getOwnerChannel(); + if (ownerChannel == null || !ownerChannel.isActive()) { + sendServiceUnavailableResponse(httpCtx); + return; + } + + // Send file request to owner + Message message = new Message(Message.TYPE_FILE_REQUEST); + JsonObject payload = new JsonObject(); + payload.addProperty("requestId", requestId); + payload.addProperty("fileId", fileId); + message.setPayload(payload); + + ownerChannel.writeAndFlush(new TextWebSocketFrame(message.toJson())); + Log.d(TAG, "Requested file stream from owner: " + fileId); + } + + private static void sendNotFoundResponse(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.NOT_FOUND, + Unpooled.wrappedBuffer("File not found".getBytes()) + ); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 14); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + addStaticCorsHeaders(response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + private static void sendServiceUnavailableResponse(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.SERVICE_UNAVAILABLE, + Unpooled.wrappedBuffer("File owner not connected".getBytes()) + ); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 24); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + addStaticCorsHeaders(response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + /** + * Handle file download request - streams file from owner client. + * URL format: /fileshare/download/{globalFileId} + * where globalFileId = userId/localFileId + * This allows stateless routing - the server doesn't need to store file info. + */ + private void handleFileDownload(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + // Extract global file ID from path: /fileshare/download/{globalFileId} + String globalFileId; + try { + globalFileId = java.net.URLDecoder.decode(pathParts[3], "UTF-8"); + } catch (Exception e) { + globalFileId = pathParts[3]; + } + + Log.d(TAG, "File download request: " + globalFileId); + + // Parse globalFileId to extract userId + // Format: userId/localFileId + String ownerId = null; + String localFileId = globalFileId; + + if (globalFileId.contains("/")) { + int slashIdx = globalFileId.indexOf('/'); + ownerId = globalFileId.substring(0, slashIdx); + localFileId = globalFileId.substring(slashIdx + 1); + } + + if (ownerId == null || ownerId.isEmpty()) { + // Fallback: try the old registry-based lookup for backward compatibility + ServiceManager.FileInfo file = serviceManager.getFile(globalFileId); + if (file != null) { + ownerId = file.getOwnerId(); + Channel ownerChannel = file.getOwnerChannel(); + if (ownerChannel != null && ownerChannel.isActive()) { + sendFileRequest(ctx, request, ownerChannel, globalFileId); + return; + } + } + sendNotFoundResponse(ctx); + return; + } + + // Find the owner's channel by userId + ServiceManager.ClientInfo ownerClient = serviceManager.getClient(ownerId); + if (ownerClient == null || ownerClient.getChannel() == null || !ownerClient.getChannel().isActive()) { + Log.w(TAG, "File owner not connected: " + ownerId); + sendServiceUnavailableResponse(ctx); + return; + } + + sendFileRequest(ctx, request, ownerClient.getChannel(), globalFileId); + } + + /** + * Send a file request to the owner and set up pending request for streaming response. + */ + private void sendFileRequest(ChannelHandlerContext ctx, FullHttpRequest request, + Channel ownerChannel, String fileId) { + // Create pending request for streaming response + String requestId = String.valueOf(System.currentTimeMillis()) + "-" + (int)(Math.random() * 10000); + PendingRequest pending = new PendingRequest(requestId, ctx, request, "fileshare"); + requestManager.addPendingRequest(pending); + + // Send file_request to owner + Message message = new Message(Message.TYPE_FILE_REQUEST); + JsonObject payload = new JsonObject(); + payload.addProperty("requestId", requestId); + payload.addProperty("fileId", fileId); + message.setPayload(payload); + + ownerChannel.writeAndFlush(new TextWebSocketFrame(message.toJson())); + Log.d(TAG, "Sent file request to owner: " + fileId); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/LocalServiceManager.java b/android/app/src/main/java/seven/lab/wstun/server/LocalServiceManager.java new file mode 100644 index 0000000..248a212 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/LocalServiceManager.java @@ -0,0 +1,433 @@ +package seven.lab.wstun.server; + +import android.content.Context; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import seven.lab.wstun.config.ServerConfig; +import seven.lab.wstun.marketplace.InstalledService; +import seven.lab.wstun.marketplace.MarketplaceService; +import seven.lab.wstun.marketplace.ServiceManifest; + +/** + * Manages local services including built-in and marketplace-installed services. + * Services can be enabled/disabled and have endpoints attached/detached dynamically. + */ +public class LocalServiceManager { + + private static final String TAG = "LocalServiceManager"; + private static final Gson gson = new Gson(); + + private final Context context; + private final ServerConfig config; + private final MarketplaceService marketplaceService; + + // Service status tracking (for running instances) + private final Map serviceStatuses = new ConcurrentHashMap<>(); + + // HTML/JS content cache + private String libwstunJs; + + /** + * Represents the status of a local service. + */ + public static class ServiceStatus { + private final String serviceName; + private String uuid; + private boolean running; + private long startedAt; + private long stoppedAt; + + public ServiceStatus(String serviceName) { + this.serviceName = serviceName; + this.running = false; + this.uuid = null; + } + + public void start() { + this.uuid = UUID.randomUUID().toString(); + this.running = true; + this.startedAt = System.currentTimeMillis(); + this.stoppedAt = 0; + } + + public void stop() { + this.running = false; + this.stoppedAt = System.currentTimeMillis(); + // Keep UUID so we can identify this was our service + } + + public String getServiceName() { return serviceName; } + public String getUuid() { return uuid; } + public boolean isRunning() { return running; } + public long getStartedAt() { return startedAt; } + public long getStoppedAt() { return stoppedAt; } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("serviceName", serviceName); + obj.addProperty("uuid", uuid); + obj.addProperty("running", running); + obj.addProperty("startedAt", startedAt); + obj.addProperty("stoppedAt", stoppedAt); + return obj; + } + } + + public LocalServiceManager(Context context, ServerConfig config) { + this.context = context; + this.config = config; + this.marketplaceService = new MarketplaceService(context); + + // Initialize service statuses for all installed services + for (String name : marketplaceService.getInstalledServices().keySet()) { + serviceStatuses.put(name, new ServiceStatus(name)); + } + + // Load libwstun.js + loadLibwstunJs(); + } + + private void loadLibwstunJs() { + try { + libwstunJs = loadAsset("libwstun.js"); + Log.i(TAG, "Loaded libwstun.js from assets"); + } catch (IOException e) { + Log.e(TAG, "Failed to load assets", e); + libwstunJs = "// libwstun.js failed to load"; + } + } + + /** + * Generate the admin HTML page dynamically. + */ + public String getAdminHtml() { + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(""); + html.append("WSTun Admin"); + html.append("

WSTun Service Manager

"); + html.append("

Use the WSTun Android app to manage services, or access services directly below.

"); + + html.append("

Installed Services

"); + for (Map.Entry entry : getInstalledServices().entrySet()) { + String name = entry.getKey(); + InstalledService svc = entry.getValue(); + String displayName = svc.getDisplayName() != null ? svc.getDisplayName() : name; + html.append("

").append(displayName).append(""); + html.append(""); + html.append(svc.isEnabled() ? "Enabled" : "Disabled").append(""); + if (svc.isEnabled()) { + html.append(" - Manage"); + } + html.append("

"); + } + html.append("
"); + + html.append(""); + return html.toString(); + } + + private String loadAsset(String path) throws IOException { + InputStream is = context.getAssets().open(path); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } + + /** + * Check if a service is enabled. + */ + public boolean isServiceEnabled(String serviceName) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + return service != null && service.isEnabled(); + } + + /** + * Get the marketplace service. + */ + public MarketplaceService getMarketplaceService() { + return marketplaceService; + } + + /** + * Get all installed services. + */ + public Map getInstalledServices() { + return marketplaceService.getInstalledServices(); + } + + /** + * Get installed service by name. + */ + public InstalledService getInstalledService(String serviceName) { + return marketplaceService.getInstalledService(serviceName); + } + + /** + * Enable a service. + */ + public boolean enableService(String serviceName) { + boolean result = marketplaceService.enableService(serviceName); + if (result && !serviceStatuses.containsKey(serviceName)) { + serviceStatuses.put(serviceName, new ServiceStatus(serviceName)); + } + return result; + } + + /** + * Disable a service. + */ + public boolean disableService(String serviceName, ServiceManager serviceManager) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null) { + return false; + } + + // Close all instances for this service + if (serviceManager != null) { + serviceManager.closeInstancesForService(serviceName); + } + + // Stop the service status + ServiceStatus status = serviceStatuses.get(serviceName); + if (status != null && status.isRunning()) { + status.stop(); + } + + return marketplaceService.disableService(serviceName); + } + + /** + * Uninstall a service. + */ + public boolean uninstallService(String serviceName, ServiceManager serviceManager) { + // Disable first + disableService(serviceName, serviceManager); + + // Remove status + serviceStatuses.remove(serviceName); + + return marketplaceService.uninstallService(serviceName); + } + + /** + * Get service status. + */ + public ServiceStatus getServiceStatus(String serviceName) { + return serviceStatuses.get(serviceName); + } + + /** + * Start a local service. + */ + public boolean startService(String serviceName) { + if (!isServiceEnabled(serviceName)) { + Log.w(TAG, "Service not enabled: " + serviceName); + return false; + } + + ServiceStatus status = serviceStatuses.get(serviceName); + if (status == null) { + Log.w(TAG, "Unknown service: " + serviceName); + return false; + } + + if (status.isRunning()) { + Log.w(TAG, "Service already running: " + serviceName); + return false; + } + + status.start(); + Log.i(TAG, "Started local service: " + serviceName + " (UUID: " + status.getUuid() + ")"); + return true; + } + + /** + * Stop a local service. + */ + public boolean stopService(String serviceName) { + ServiceStatus status = serviceStatuses.get(serviceName); + if (status == null) { + Log.w(TAG, "Unknown service: " + serviceName); + return false; + } + + if (!status.isRunning()) { + Log.w(TAG, "Service not running: " + serviceName); + return false; + } + + status.stop(); + Log.i(TAG, "Stopped local service: " + serviceName); + return true; + } + + /** + * Stop a service by UUID (ensures it's exactly our service). + */ + public boolean stopServiceByUuid(String serviceName, String uuid) { + ServiceStatus status = serviceStatuses.get(serviceName); + if (status == null) { + return false; + } + + if (!uuid.equals(status.getUuid())) { + Log.w(TAG, "UUID mismatch for service: " + serviceName); + return false; + } + + return stopService(serviceName); + } + + /** + * Get the service management HTML page for a service. + * This returns the service controller page (index.html) which: + * - Registers as a SERVICE with the server + * - Shows in Android UI + * - Manages user clients + */ + public String getServicePageHtml(String serviceName, String serverUrl) { + if (!isServiceEnabled(serviceName)) { + return getServiceDisabledHtml(serviceName); + } + + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null) { + return getServiceDisabledHtml(serviceName); + } + + // Get the /service endpoint file + String content = service.getFileContent("/service"); + if (content == null || content.isEmpty()) { + return getServiceDisabledHtml(serviceName); + } + return content; + } + + /** + * Get the main service HTML (the user client UI). + * This is served directly from server when service is registered. + */ + public String getServiceMainHtml(String serviceName) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null) { + return null; + } + + return service.getFileContent("/main"); + } + + /** + * Get file content for any endpoint of a service. + */ + public String getServiceFileContent(String serviceName, String path) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null || !service.isEnabled()) { + return null; + } + + return service.getFileContent(path); + } + + /** + * Get list of all endpoints for a service. + */ + public List getServiceEndpoints(String serviceName) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null || service.getManifest() == null) { + return new ArrayList<>(); + } + return service.getManifest().getEndpoints(); + } + + /** + * Get the libwstun.js library content. + */ + public String getLibWstunJs() { + return libwstunJs; + } + + private String getServiceDisabledHtml(String serviceName) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + String displayName = service != null ? service.getDisplayName() : serviceName; + + return "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " " + displayName + " Service - Disabled\n" + + " \n" + + "\n" + + "\n" + + "
\n" + + "

Service Disabled

\n" + + "

The " + displayName + " service is not enabled on this server.

\n" + + "

Enable it in the WSTun app service management.

\n" + + "
\n" + + "\n" + + ""; + } + + /** + * Get all installed services as JSON array. + */ + public JsonArray getInstalledServicesJson() { + JsonArray arr = new JsonArray(); + for (Map.Entry entry : marketplaceService.getInstalledServices().entrySet()) { + JsonObject obj = entry.getValue().toJson(); + obj.addProperty("name", entry.getKey()); + + // Add running status + ServiceStatus status = serviceStatuses.get(entry.getKey()); + if (status != null) { + obj.addProperty("running", status.isRunning()); + if (status.getUuid() != null) { + obj.addProperty("uuid", status.getUuid()); + } + } + arr.add(obj); + } + return arr; + } + + /** + * Shutdown resources. + */ + public void shutdown() { + if (marketplaceService != null) { + marketplaceService.shutdown(); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/NettyServer.java b/android/app/src/main/java/seven/lab/wstun/server/NettyServer.java new file mode 100644 index 0000000..0a5b8c4 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/NettyServer.java @@ -0,0 +1,223 @@ +package seven.lab.wstun.server; + +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.DefaultThreadFactory; +import seven.lab.wstun.config.ServerConfig; + +/** + * Netty-based HTTP/WebSocket server with optional HTTPS support. + * Uses dedicated threads with high priority to ensure responsiveness + * even when the Android app is in the background. + */ +public class NettyServer { + + private static final String TAG = "NettyServer"; + + private final Context context; + private final ServerConfig config; + private final ServiceManager serviceManager; + private final RequestManager requestManager; + private final LocalServiceManager localServiceManager; + + private EventLoopGroup bossGroup; + private EventLoopGroup workerGroup; + private Channel serverChannel; + private boolean running = false; + private PowerManager.WakeLock wakeLock; + + public NettyServer(Context context, ServerConfig config, ServiceManager serviceManager) { + this.context = context; + this.config = config; + this.serviceManager = serviceManager; + this.requestManager = new RequestManager(); + this.localServiceManager = new LocalServiceManager(context, config); + + // Link ServiceManager to RequestManager for cleanup on disconnect + this.serviceManager.setRequestManager(this.requestManager); + } + + /** + * Start the server. + */ + public synchronized void start() throws Exception { + if (running) { + Log.w(TAG, "Server already running"); + return; + } + + // Acquire a partial wake lock to keep the CPU running for network I/O + // This is essential for the server to respond when the app is in the background + try { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (powerManager != null) { + wakeLock = powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "WSTun:NettyServer" + ); + wakeLock.acquire(); + Log.i(TAG, "Acquired wake lock for background operation"); + } + } catch (Exception e) { + Log.w(TAG, "Failed to acquire wake lock: " + e.getMessage()); + } + + int port = config.getPort(); + boolean ssl = config.isHttpsEnabled(); + + // Get SSL context if needed + SslContext sslContext = null; + if (ssl) { + sslContext = SslContextFactory.getSslContext(context); + } + final SslContext finalSslContext = sslContext; + final String corsOrigins = config.getCorsOrigins(); + final int serverPort = port; + final LocalServiceManager localSvcMgr = localServiceManager; + final ServerConfig serverConfig = config; + + // Use custom thread factory to create high-priority daemon threads + // This ensures Netty threads keep running even when app is backgrounded + DefaultThreadFactory bossThreadFactory = new DefaultThreadFactory("wstun-boss", true, Thread.MAX_PRIORITY); + DefaultThreadFactory workerThreadFactory = new DefaultThreadFactory("wstun-worker", true, Thread.MAX_PRIORITY); + + bossGroup = new NioEventLoopGroup(1, bossThreadFactory); + // Use more worker threads to handle concurrent HTTP and WebSocket connections + workerGroup = new NioEventLoopGroup(Math.max(4, Runtime.getRuntime().availableProcessors() * 2), workerThreadFactory); + + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 1024) + .option(ChannelOption.SO_REUSEADDR, true) + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childOption(ChannelOption.TCP_NODELAY, true) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + Log.d(TAG, "New connection from: " + ch.remoteAddress()); + ChannelPipeline pipeline = ch.pipeline(); + + // SSL handler + if (finalSslContext != null) { + pipeline.addLast("ssl", finalSslContext.newHandler(ch.alloc())); + } + + // HTTP codec + pipeline.addLast("http-codec", new HttpServerCodec()); + + // Aggregate HTTP message parts into FullHttpRequest + pipeline.addLast("http-aggregator", new HttpObjectAggregator(65536)); + + // Idle state handler - longer timeouts for better stability + // Read idle: 120s, Write idle: 60s, All idle: 0 (disabled) + pipeline.addLast("idle", new IdleStateHandler(120, 60, 0)); + + // HTTP/WebSocket handler with CORS configuration and local service support + pipeline.addLast("http-handler", + new HttpHandler(serviceManager, requestManager, localSvcMgr, ssl, corsOrigins, serverPort, serverConfig)); + } + }); + + serverChannel = bootstrap.bind(port).sync().channel(); + running = true; + + Log.i(TAG, "Server started on port " + port + (ssl ? " (HTTPS)" : " (HTTP)")); + } + + /** + * Stop the server. + */ + public synchronized void stop() { + if (!running) { + return; + } + + running = false; + + // Close all service connections + serviceManager.clear(); + + // Shutdown request manager + requestManager.shutdown(); + + // Close server channel + if (serverChannel != null) { + try { + serverChannel.close().sync(); + } catch (InterruptedException e) { + Log.e(TAG, "Error closing server channel", e); + } + } + + // Shutdown event loops with timeout + if (workerGroup != null) { + workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); + } + if (bossGroup != null) { + bossGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); + } + + // Release wake lock + if (wakeLock != null && wakeLock.isHeld()) { + try { + wakeLock.release(); + Log.i(TAG, "Released wake lock"); + } catch (Exception e) { + Log.w(TAG, "Failed to release wake lock: " + e.getMessage()); + } + wakeLock = null; + } + + Log.i(TAG, "Server stopped"); + } + + /** + * Check if server is running. + */ + public boolean isRunning() { + return running; + } + + /** + * Get the service manager. + */ + public ServiceManager getServiceManager() { + return serviceManager; + } + + /** + * Get the request manager. + */ + public RequestManager getRequestManager() { + return requestManager; + } + + /** + * Get the local service manager. + */ + public LocalServiceManager getLocalServiceManager() { + return localServiceManager; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/PendingRequest.java b/android/app/src/main/java/seven/lab/wstun/server/PendingRequest.java new file mode 100644 index 0000000..d0a7e96 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/PendingRequest.java @@ -0,0 +1,48 @@ +package seven.lab.wstun.server; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpRequest; + +/** + * Represents a pending HTTP request waiting for response from a service client. + */ +public class PendingRequest { + + private final String requestId; + private final ChannelHandlerContext ctx; + private final HttpRequest request; + private final String serviceName; + private final long timestamp; + + public PendingRequest(String requestId, ChannelHandlerContext ctx, HttpRequest request, String serviceName) { + this.requestId = requestId; + this.ctx = ctx; + this.request = request; + this.serviceName = serviceName; + this.timestamp = System.currentTimeMillis(); + } + + public String getRequestId() { + return requestId; + } + + public ChannelHandlerContext getCtx() { + return ctx; + } + + public HttpRequest getRequest() { + return request; + } + + public String getServiceName() { + return serviceName; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isTimedOut(long timeoutMs) { + return System.currentTimeMillis() - timestamp > timeoutMs; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/RequestManager.java b/android/app/src/main/java/seven/lab/wstun/server/RequestManager.java new file mode 100644 index 0000000..ab98f71 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/RequestManager.java @@ -0,0 +1,152 @@ +package seven.lab.wstun.server; + +import android.util.Log; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +/** + * Manages pending HTTP requests that are being relayed to service clients. + */ +public class RequestManager { + + private static final String TAG = "RequestManager"; + private static final long REQUEST_TIMEOUT_MS = 10000; // 10 seconds - shorter timeout for better responsiveness + + private final Map pendingRequests = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler; + + public RequestManager() { + scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleAtFixedRate(this::cleanupTimedOutRequests, 5, 5, TimeUnit.SECONDS); + } + + /** + * Register a pending request. + */ + public void addPendingRequest(PendingRequest request) { + pendingRequests.put(request.getRequestId(), request); + Log.d(TAG, "Added pending request: " + request.getRequestId()); + } + + /** + * Get and remove a pending request. + */ + public PendingRequest removePendingRequest(String requestId) { + PendingRequest request = pendingRequests.remove(requestId); + if (request != null) { + Log.d(TAG, "Removed pending request: " + requestId); + } + return request; + } + + /** + * Get a pending request without removing it. + */ + public PendingRequest getPendingRequest(String requestId) { + return pendingRequests.get(requestId); + } + + /** + * Fail all pending requests for a specific service (when service disconnects). + */ + public void failRequestsForService(String serviceName) { + Iterator> iterator = pendingRequests.entrySet().iterator(); + int count = 0; + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + PendingRequest request = entry.getValue(); + + if (serviceName.equals(request.getServiceName())) { + iterator.remove(); + count++; + + // Send error response on the correct event loop + if (request.getCtx().channel().isActive()) { + request.getCtx().executor().execute(() -> { + if (request.getCtx().channel().isActive()) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.SERVICE_UNAVAILABLE, + Unpooled.copiedBuffer("Service disconnected".getBytes()) + ); + response.headers().set("Content-Type", "text/plain"); + response.headers().setInt("Content-Length", 20); + request.getCtx().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + }); + } + } + } + + if (count > 0) { + Log.w(TAG, "Failed " + count + " pending requests for disconnected service: " + serviceName); + } + } + + /** + * Clean up timed out requests. + */ + private void cleanupTimedOutRequests() { + Iterator> iterator = pendingRequests.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + PendingRequest request = entry.getValue(); + + if (request.isTimedOut(REQUEST_TIMEOUT_MS)) { + iterator.remove(); + Log.w(TAG, "Request timed out: " + request.getRequestId()); + + // Send timeout response on the correct event loop + if (request.getCtx().channel().isActive()) { + request.getCtx().executor().execute(() -> { + if (request.getCtx().channel().isActive()) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.GATEWAY_TIMEOUT, + Unpooled.copiedBuffer("Request timed out".getBytes()) + ); + request.getCtx().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + }); + } + } + } + } + + /** + * Shutdown the request manager. + */ + public void shutdown() { + scheduler.shutdown(); + + // Send error response to all pending requests on the correct event loop + for (PendingRequest request : pendingRequests.values()) { + if (request.getCtx().channel().isActive()) { + request.getCtx().executor().execute(() -> { + if (request.getCtx().channel().isActive()) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.SERVICE_UNAVAILABLE, + Unpooled.copiedBuffer("Server shutting down".getBytes()) + ); + request.getCtx().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + }); + } + } + pendingRequests.clear(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/ServiceManager.java b/android/app/src/main/java/seven/lab/wstun/server/ServiceManager.java new file mode 100644 index 0000000..bc0acdd --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/ServiceManager.java @@ -0,0 +1,1106 @@ +package seven.lab.wstun.server; + +import android.util.Log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import seven.lab.wstun.protocol.Message; +import seven.lab.wstun.protocol.ServiceRegistration; + +/** + * Manages registered services, instances, and their WebSocket connections. + * + * Architecture: + * - Service: A type of service (e.g., fileshare, chat) + * - ServiceInstance: A specific room/session with UUID, name, and optional token + * - User: Connected to a specific instance + */ +public class ServiceManager { + + private static final String TAG = "ServiceManager"; + + // Service name -> ServiceEntry (the service provider) + private final Map services = new ConcurrentHashMap<>(); + + // Instance UUID -> ServiceInstance + private final Map instances = new ConcurrentHashMap<>(); + + // Service name -> List of instance UUIDs + private final Map> serviceInstances = new ConcurrentHashMap<>(); + + // Channel -> Service name (for cleanup on disconnect) + private final Map channelToService = new ConcurrentHashMap<>(); + + // Channel -> Instance UUID (for instance owner cleanup) + private final Map channelToInstance = new ConcurrentHashMap<>(); + + // File registry: fileId -> FileInfo (for relay file sharing) + private final Map fileRegistry = new ConcurrentHashMap<>(); + + // Channel -> List of file IDs owned by that channel + private final Map> channelToFiles = new ConcurrentHashMap<>(); + + // Client registry: userId -> ClientInfo (for user clients) + private final Map clients = new ConcurrentHashMap<>(); + + // Channel -> userId (for client cleanup on disconnect) + private final Map channelToClient = new ConcurrentHashMap<>(); + + // Chat users: userId -> ChatUser + private final Map chatUsers = new ConcurrentHashMap<>(); + + private ServiceChangeListener listener; + private RequestManager requestManager; + + /** + * Represents a service instance (room/session). + */ + public static class ServiceInstance { + private final String uuid; + private final String serviceName; + private final String name; + private final String token; + private Channel ownerChannel; + private final long createdAt; + private final List userIds = new ArrayList<>(); + // Grace period for reconnection (10 seconds) + private static final long OWNER_GRACE_PERIOD_MS = 10000; + private long ownerDisconnectedAt = 0; + + public ServiceInstance(String uuid, String serviceName, String name, String token, Channel ownerChannel) { + this.uuid = uuid; + this.serviceName = serviceName; + this.name = name; + this.token = token; + this.ownerChannel = ownerChannel; + this.createdAt = System.currentTimeMillis(); + } + + public String getUuid() { return uuid; } + public String getServiceName() { return serviceName; } + public String getName() { return name; } + public String getToken() { return token; } + public Channel getOwnerChannel() { return ownerChannel; } + public long getCreatedAt() { return createdAt; } + public List getUserIds() { return userIds; } + + public boolean hasToken() { + return token != null && !token.isEmpty(); + } + + public boolean validateToken(String inputToken) { + if (!hasToken()) return true; + return token.equals(inputToken); + } + + public void addUser(String userId) { + if (!userIds.contains(userId)) { + userIds.add(userId); + } + } + + public void removeUser(String userId) { + userIds.remove(userId); + } + + public boolean isOwnerConnected() { + return ownerChannel != null && ownerChannel.isActive(); + } + + /** + * Check if instance is still valid (owner connected or within grace period). + */ + public boolean isValid() { + if (isOwnerConnected()) { + return true; + } + // Allow grace period for owner to reconnect + if (ownerDisconnectedAt > 0) { + return (System.currentTimeMillis() - ownerDisconnectedAt) < OWNER_GRACE_PERIOD_MS; + } + return false; + } + + /** + * Mark owner as disconnected but keep instance alive for grace period. + */ + public void markOwnerDisconnected() { + this.ownerChannel = null; + this.ownerDisconnectedAt = System.currentTimeMillis(); + } + + /** + * Reconnect owner to this instance. + */ + public void reconnectOwner(Channel newOwnerChannel) { + this.ownerChannel = newOwnerChannel; + this.ownerDisconnectedAt = 0; + } + + /** + * Check if owner can be reclaimed (within grace period). + */ + public boolean canReclaim() { + return !isOwnerConnected() && ownerDisconnectedAt > 0 && + (System.currentTimeMillis() - ownerDisconnectedAt) < OWNER_GRACE_PERIOD_MS; + } + + public int getUserCount() { + return userIds.size(); + } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("uuid", uuid); + obj.addProperty("service", serviceName); + obj.addProperty("name", name); + obj.addProperty("hasToken", hasToken()); + obj.addProperty("createdAt", createdAt); + obj.addProperty("userCount", userIds.size()); + obj.addProperty("ownerConnected", isOwnerConnected()); + return obj; + } + } + + /** + * Represents a connected client (user, not service). + */ + public static class ClientInfo { + private final String userId; + private final String clientType; + private final String instanceUuid; + private final Channel channel; + private final long connectedAt; + + public ClientInfo(String userId, String clientType, String instanceUuid, Channel channel) { + this.userId = userId; + this.clientType = clientType; + this.instanceUuid = instanceUuid; + this.channel = channel; + this.connectedAt = System.currentTimeMillis(); + } + + public String getUserId() { return userId; } + public String getClientType() { return clientType; } + public String getInstanceUuid() { return instanceUuid; } + public Channel getChannel() { return channel; } + public long getConnectedAt() { return connectedAt; } + } + + /** + * Represents a chat user. + */ + public static class ChatUser { + private final String userId; + private String name; + private final Channel channel; + private final long joinedAt; + + public ChatUser(String userId, String name, Channel channel) { + this.userId = userId; + this.name = name; + this.channel = channel; + this.joinedAt = System.currentTimeMillis(); + } + + public String getUserId() { return userId; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Channel getChannel() { return channel; } + public long getJoinedAt() { return joinedAt; } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("id", userId); + obj.addProperty("name", name); + obj.addProperty("joinedAt", joinedAt); + return obj; + } + } + + /** + * Represents a file registered for relay sharing. + */ + public static class FileInfo { + private final String fileId; + private final String filename; + private final long size; + private final String mimeType; + private final String ownerId; + private final Channel ownerChannel; + + public FileInfo(String fileId, String filename, long size, String mimeType, + String ownerId, Channel ownerChannel) { + this.fileId = fileId; + this.filename = filename; + this.size = size; + this.mimeType = mimeType; + this.ownerId = ownerId; + this.ownerChannel = ownerChannel; + } + + public String getFileId() { return fileId; } + public String getFilename() { return filename; } + public long getSize() { return size; } + public String getMimeType() { return mimeType; } + public String getOwnerId() { return ownerId; } + public Channel getOwnerChannel() { return ownerChannel; } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("id", fileId); + obj.addProperty("filename", filename); + obj.addProperty("size", size); + obj.addProperty("mimeType", mimeType); + obj.addProperty("ownerId", ownerId); + return obj; + } + } + + public interface ServiceChangeListener { + void onServiceAdded(ServiceEntry service); + void onServiceRemoved(ServiceEntry service); + } + + /** + * Represents a registered service. + */ + public static class ServiceEntry { + private final String name; + private final String type; + private final String description; + private final Channel channel; + private final ServiceRegistration registration; + private final long registeredAt; + private final String authToken; + + public ServiceEntry(ServiceRegistration registration, Channel channel) { + this.name = registration.getName(); + this.type = registration.getType(); + this.description = registration.getDescription(); + this.channel = channel; + this.registration = registration; + this.registeredAt = System.currentTimeMillis(); + this.authToken = registration.getAuthToken(); + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + public Channel getChannel() { + return channel; + } + + public ServiceRegistration getRegistration() { + return registration; + } + + public long getRegisteredAt() { + return registeredAt; + } + + public boolean isConnected() { + return channel != null && channel.isActive(); + } + + public String getAuthToken() { + return authToken; + } + + public boolean hasAuth() { + return authToken != null && !authToken.isEmpty(); + } + + /** + * Validate client auth token for this service. + * Returns true if no auth required or token matches. + */ + public boolean validateClientAuth(String token) { + if (!hasAuth()) { + return true; + } + return authToken.equals(token); + } + } + + public void setListener(ServiceChangeListener listener) { + this.listener = listener; + } + + public void setRequestManager(RequestManager requestManager) { + this.requestManager = requestManager; + } + + // Reserved service names that cannot be used + private static final java.util.Set RESERVED_NAMES = new java.util.HashSet<>( + java.util.Arrays.asList( + "debug", "admin", "_api", "ws", "websocket", + "api", "system", "config", "static", "assets", + "libwstun.js" + ) + ); + + /** + * Check if a service name is reserved. + */ + public static boolean isReservedName(String name) { + if (name == null) return true; + return RESERVED_NAMES.contains(name.toLowerCase()); + } + + /** + * Register a new service. + */ + public boolean registerService(ServiceRegistration registration, Channel channel) { + String name = registration.getName(); + + if (name == null || name.isEmpty()) { + Log.w(TAG, "Service registration failed: no name provided"); + return false; + } + + // Check for reserved names + if (isReservedName(name)) { + Log.w(TAG, "Service registration failed: reserved name: " + name); + return false; + } + + // Check if service with same name exists + if (services.containsKey(name)) { + Log.w(TAG, "Service already exists: " + name); + return false; + } + + ServiceEntry entry = new ServiceEntry(registration, channel); + services.put(name, entry); + channelToService.put(channel, name); + + Log.i(TAG, "Service registered: " + name + " (type: " + registration.getType() + ")"); + + if (listener != null) { + listener.onServiceAdded(entry); + } + + return true; + } + + /** + * Unregister a service by name. + */ + public void unregisterService(String name) { + ServiceEntry entry = services.remove(name); + if (entry != null) { + channelToService.remove(entry.getChannel()); + Log.i(TAG, "Service unregistered: " + name); + + if (listener != null) { + listener.onServiceRemoved(entry); + } + } + } + + /** + * Create a new service instance (room/session). + */ + public ServiceInstance createInstance(String serviceName, String name, String token, Channel ownerChannel) { + String uuid = java.util.UUID.randomUUID().toString().substring(0, 8); + + ServiceInstance instance = new ServiceInstance(uuid, serviceName, name, token, ownerChannel); + instances.put(uuid, instance); + channelToInstance.put(ownerChannel, uuid); + + // Track instances per service + serviceInstances.computeIfAbsent(serviceName, k -> new ArrayList<>()).add(uuid); + + Log.i(TAG, "Instance created: " + uuid + " (" + name + ") for service " + serviceName); + return instance; + } + + /** + * Get an instance by UUID. + */ + public ServiceInstance getInstance(String uuid) { + return instances.get(uuid); + } + + /** + * Get all instances for a service. + * Returns instances where owner is connected OR within grace period. + */ + public List getInstancesForService(String serviceName) { + List uuids = serviceInstances.get(serviceName); + if (uuids == null) return new ArrayList<>(); + + List result = new ArrayList<>(); + for (String uuid : uuids) { + ServiceInstance inst = instances.get(uuid); + if (inst != null && inst.isValid()) { + result.add(inst); + } + } + return result; + } + + /** + * Remove an instance. + */ + public void removeInstance(String uuid) { + ServiceInstance instance = instances.remove(uuid); + if (instance != null) { + channelToInstance.remove(instance.getOwnerChannel()); + List uuids = serviceInstances.get(instance.getServiceName()); + if (uuids != null) { + uuids.remove(uuid); + } + Log.i(TAG, "Instance removed: " + uuid); + } + } + + /** + * Destroy an instance (kick all users and close owner connection). + */ + public void destroyInstance(String uuid) { + ServiceInstance instance = instances.get(uuid); + if (instance == null) { + return; + } + + // Kick all users in the instance + for (String userId : new ArrayList<>(instance.getUserIds())) { + kickClient(userId); + } + + // Close owner connection + Channel ownerChannel = instance.getOwnerChannel(); + if (ownerChannel != null && ownerChannel.isActive()) { + Message msg = new Message("instance_destroyed"); + JsonObject payload = new JsonObject(); + payload.addProperty("uuid", uuid); + payload.addProperty("reason", "Instance destroyed by administrator"); + msg.setPayload(payload); + ownerChannel.writeAndFlush(new TextWebSocketFrame(msg.toJson())); + ownerChannel.close(); + } + + // Remove instance + removeInstance(uuid); + Log.i(TAG, "Instance destroyed: " + uuid); + } + + /** + * Close all instances for a service (when disabling/uninstalling). + */ + public void closeInstancesForService(String serviceName) { + List uuids = serviceInstances.get(serviceName); + if (uuids == null || uuids.isEmpty()) { + return; + } + + // Make a copy to avoid concurrent modification + List toRemove = new ArrayList<>(uuids); + + for (String uuid : toRemove) { + ServiceInstance instance = instances.get(uuid); + if (instance != null) { + // Kick all users in the instance + for (String userId : new ArrayList<>(instance.getUserIds())) { + kickClient(userId); + } + + // Close owner connection + Channel ownerChannel = instance.getOwnerChannel(); + if (ownerChannel != null && ownerChannel.isActive()) { + Message msg = new Message("service_disabled"); + JsonObject payload = new JsonObject(); + payload.addProperty("serviceName", serviceName); + payload.addProperty("reason", "Service disabled"); + msg.setPayload(payload); + ownerChannel.writeAndFlush(new TextWebSocketFrame(msg.toJson())); + ownerChannel.close(); + } + + // Remove instance + removeInstance(uuid); + } + } + + Log.i(TAG, "Closed all instances for service: " + serviceName); + } + + /** + * Get count of running instances for a service. + */ + public int getInstanceCountForService(String serviceName) { + return getInstancesForService(serviceName).size(); + } + + /** + * Clean up instance for a disconnected channel. + * Uses grace period to allow owner to reconnect. + */ + private void cleanupInstanceForChannel(Channel channel) { + String uuid = channelToInstance.remove(channel); + if (uuid != null) { + ServiceInstance instance = instances.get(uuid); + if (instance != null) { + // Mark owner as disconnected but keep instance for grace period + instance.markOwnerDisconnected(); + Log.i(TAG, "Instance owner disconnected, grace period started: " + uuid); + + // Schedule cleanup after grace period + new Thread(() -> { + try { + Thread.sleep(ServiceInstance.OWNER_GRACE_PERIOD_MS + 1000); + } catch (InterruptedException e) { + return; + } + // Check if instance still needs cleanup (owner didn't reconnect) + ServiceInstance inst = instances.get(uuid); + if (inst != null && !inst.isOwnerConnected()) { + instances.remove(uuid); + List uuids = serviceInstances.get(inst.getServiceName()); + if (uuids != null) { + uuids.remove(uuid); + } + Log.i(TAG, "Instance cleaned up after grace period: " + uuid); + } + }).start(); + } + } + } + + /** + * Reclaim an instance that lost its owner (within grace period). + * @return true if reclaim successful, false otherwise + */ + public boolean reclaimInstance(String uuid, Channel newOwnerChannel) { + ServiceInstance instance = instances.get(uuid); + if (instance == null) { + return false; + } + + if (!instance.canReclaim()) { + return false; + } + + instance.reconnectOwner(newOwnerChannel); + channelToInstance.put(newOwnerChannel, uuid); + Log.i(TAG, "Instance reclaimed by new owner: " + uuid); + return true; + } + + /** + * Handle channel disconnect - unregister associated service and clean up files. + */ + public void onChannelDisconnect(Channel channel) { + // Clean up files owned by this channel + cleanupFilesForChannel(channel); + + // Clean up client if any + cleanupClientForChannel(channel); + + // Clean up instance if any + cleanupInstanceForChannel(channel); + + String serviceName = channelToService.remove(channel); + if (serviceName != null) { + ServiceEntry entry = services.remove(serviceName); + if (entry != null) { + Log.i(TAG, "Service disconnected: " + serviceName); + + // Fail any pending HTTP requests for this service + if (requestManager != null) { + requestManager.failRequestsForService(serviceName); + } + + if (listener != null) { + listener.onServiceRemoved(entry); + } + } + } + } + + /** + * Get a service by name. + */ + public ServiceEntry getService(String name) { + return services.get(name); + } + + /** + * Get all registered services. + */ + public List getAllServices() { + return new ArrayList<>(services.values()); + } + + /** + * Check if a service exists. + */ + public boolean hasService(String name) { + return services.containsKey(name); + } + + /** + * Get the number of registered services. + */ + public int getServiceCount() { + return services.size(); + } + + /** + * Kick (disconnect) a service. + */ + public void kickService(String name) { + ServiceEntry entry = services.get(name); + if (entry != null && entry.getChannel() != null) { + entry.getChannel().close(); + // The channel close will trigger onChannelDisconnect + } + } + + /** + * Clear all services. + */ + public void clear() { + for (ServiceEntry entry : services.values()) { + if (entry.getChannel() != null) { + entry.getChannel().close(); + } + } + services.clear(); + channelToService.clear(); + fileRegistry.clear(); + channelToFiles.clear(); + } + + // ==================== File Registry Methods ==================== + + /** + * Register a file for relay sharing. + */ + public void registerFile(String fileId, String filename, long size, String mimeType, + String ownerId, Channel ownerChannel) { + FileInfo info = new FileInfo(fileId, filename, size, mimeType, ownerId, ownerChannel); + fileRegistry.put(fileId, info); + + // Track files by channel for cleanup + channelToFiles.computeIfAbsent(ownerChannel, k -> new ArrayList<>()).add(fileId); + + Log.i(TAG, "File registered: " + filename + " (" + fileId + ") by " + ownerId); + + // Broadcast updated file list to all fileshare service clients + broadcastFileList(); + } + + /** + * Unregister a file from relay sharing. + */ + public void unregisterFile(String fileId) { + FileInfo info = fileRegistry.remove(fileId); + if (info != null) { + List files = channelToFiles.get(info.getOwnerChannel()); + if (files != null) { + files.remove(fileId); + } + Log.i(TAG, "File unregistered: " + info.getFilename() + " (" + fileId + ")"); + broadcastFileList(); + } + } + + /** + * Get file info by ID. + */ + public FileInfo getFile(String fileId) { + return fileRegistry.get(fileId); + } + + /** + * Get all registered files. + */ + public List getAllFiles() { + return new ArrayList<>(fileRegistry.values()); + } + + /** + * Handle channel disconnect - clean up files owned by this channel. + */ + private void cleanupFilesForChannel(Channel channel) { + List files = channelToFiles.remove(channel); + if (files != null && !files.isEmpty()) { + for (String fileId : files) { + fileRegistry.remove(fileId); + } + Log.i(TAG, "Cleaned up " + files.size() + " files for disconnected channel"); + broadcastFileList(); + } + } + + /** + * Broadcast updated file list to all connected fileshare clients. + */ + public void broadcastFileList() { + // Build file list JSON + JsonArray filesArray = new JsonArray(); + for (FileInfo file : fileRegistry.values()) { + filesArray.add(file.toJson()); + } + + Message message = new Message(Message.TYPE_FILE_LIST); + JsonObject payload = new JsonObject(); + payload.add("files", filesArray); + message.setPayload(payload); + + String json = message.toJson(); + + // Send to all fileshare clients + for (ClientInfo client : clients.values()) { + if ("fileshare".equals(client.getClientType()) && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + + // Also send to any other channels that have registered files + for (Channel channel : channelToFiles.keySet()) { + if (channel.isActive()) { + channel.writeAndFlush(new TextWebSocketFrame(json)); + } + } + } + + // ==================== Client Registry Methods ==================== + + /** + * Register a user client (not a service). + */ + public void registerClient(String userId, String clientType, Channel channel) { + registerClient(userId, clientType, null, channel); + } + + /** + * Register a user client to a specific instance. + */ + public void registerClient(String userId, String clientType, String instanceUuid, Channel channel) { + ClientInfo info = new ClientInfo(userId, clientType, instanceUuid, channel); + clients.put(userId, info); + channelToClient.put(channel, userId); + + // Add user to instance if specified + if (instanceUuid != null) { + ServiceInstance instance = instances.get(instanceUuid); + if (instance != null) { + instance.addUser(userId); + } + } + + Log.i(TAG, "Client registered: " + userId + " (type: " + clientType + + (instanceUuid != null ? ", instance: " + instanceUuid : "") + ")"); + + // Notify the instance owner that a client connected + notifyInstanceOwner(instanceUuid, clientType, "client_connected", userId); + } + + /** + * Unregister a client. + */ + public void unregisterClient(String userId) { + ClientInfo info = clients.remove(userId); + if (info != null) { + channelToClient.remove(info.getChannel()); + Log.i(TAG, "Client unregistered: " + userId); + } + } + + /** + * Get a client by userId. + */ + public ClientInfo getClient(String userId) { + return clients.get(userId); + } + + /** + * Get all clients of a specific type. + */ + public List getClientsByType(String clientType) { + List result = new ArrayList<>(); + for (ClientInfo client : clients.values()) { + if (clientType.equals(client.getClientType())) { + result.add(client); + } + } + return result; + } + + /** + * Get a client by their channel. + */ + public ClientInfo getClientByChannel(Channel channel) { + String userId = channelToClient.get(channel); + if (userId != null) { + return clients.get(userId); + } + return null; + } + + /** + * Get all clients in a specific instance. + */ + public List getClientsInInstance(String instanceUuid) { + List result = new ArrayList<>(); + ServiceInstance instance = instances.get(instanceUuid); + if (instance == null) { + return result; + } + + for (String userId : instance.getUserIds()) { + ClientInfo client = clients.get(userId); + if (client != null) { + result.add(client); + } + } + return result; + } + + /** + * Kick a client by userId. + * @return true if client was found and kicked + */ + public boolean kickClient(String userId) { + ClientInfo client = clients.get(userId); + if (client == null) { + return false; + } + + // Send kick message to client + Message kickMsg = new Message("kick"); + JsonObject payload = new JsonObject(); + payload.addProperty("reason", "Kicked by administrator"); + kickMsg.setPayload(payload); + + if (client.getChannel() != null && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(kickMsg.toJson())); + client.getChannel().close(); + } + + // Clean up + clients.remove(userId); + channelToClient.remove(client.getChannel()); + + // Also remove from chat users if applicable + ChatUser chatUser = chatUsers.remove(userId); + if (chatUser != null) { + broadcastChatLeave(userId, chatUser.getName()); + broadcastChatUserList(); + } + + Log.i(TAG, "Kicked client: " + userId); + return true; + } + + /** + * Clean up clients for a disconnected channel. + */ + private void cleanupClientForChannel(Channel channel) { + String userId = channelToClient.remove(channel); + if (userId != null) { + ClientInfo client = clients.remove(userId); + Log.i(TAG, "Cleaned up client for disconnected channel: " + userId); + + // Remove from instance + if (client != null && client.getInstanceUuid() != null) { + ServiceInstance instance = instances.get(client.getInstanceUuid()); + if (instance != null) { + instance.removeUser(userId); + } + notifyInstanceOwner(client.getInstanceUuid(), client.getClientType(), "client_disconnected", userId); + } + + // Also clean up chat user if any + ChatUser chatUser = chatUsers.remove(userId); + if (chatUser != null) { + broadcastChatLeave(userId, chatUser.getName()); + broadcastChatUserList(); + } + } + } + + /** + * Notify an instance owner when a client connects or disconnects. + */ + private void notifyInstanceOwner(String instanceUuid, String serviceName, String eventType, String userId) { + ServiceInstance instance = instanceUuid != null ? instances.get(instanceUuid) : null; + Channel ownerChannel = instance != null ? instance.getOwnerChannel() : null; + + // If no instance, fall back to service channel + if (ownerChannel == null) { + ServiceEntry service = services.get(serviceName); + if (service != null && service.isConnected()) { + ownerChannel = service.getChannel(); + } + } + + if (ownerChannel != null && ownerChannel.isActive()) { + Message message = new Message(eventType); + message.setService(serviceName); + JsonObject payload = new JsonObject(); + payload.addProperty("userId", userId); + if (instanceUuid != null) { + payload.addProperty("instanceUuid", instanceUuid); + } + message.setPayload(payload); + ownerChannel.writeAndFlush(new TextWebSocketFrame(message.toJson())); + } + } + + // ==================== Chat User Methods ==================== + + /** + * Register a chat user. + */ + public void registerChatUser(String userId, String name, Channel channel) { + ChatUser user = new ChatUser(userId, name, channel); + chatUsers.put(userId, user); + Log.i(TAG, "Chat user registered: " + name + " (" + userId + ")"); + } + + /** + * Unregister a chat user. + */ + public void unregisterChatUser(String userId) { + chatUsers.remove(userId); + Log.i(TAG, "Chat user unregistered: " + userId); + } + + /** + * Get chat user name. + */ + public String getChatUserName(String userId) { + ChatUser user = chatUsers.get(userId); + return user != null ? user.getName() : null; + } + + /** + * Broadcast chat user list to all chat clients. + */ + public void broadcastChatUserList() { + JsonArray usersArray = new JsonArray(); + for (ChatUser user : chatUsers.values()) { + usersArray.add(user.toJson()); + } + + Message message = new Message(Message.TYPE_CHAT_USER_LIST); + JsonObject payload = new JsonObject(); + payload.add("users", usersArray); + message.setPayload(payload); + + String json = message.toJson(); + + // Send to all chat clients + for (ClientInfo client : clients.values()) { + if ("chat".equals(client.getClientType()) && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + } + + /** + * Broadcast chat join notification. + */ + public void broadcastChatJoin(String userId, String name) { + Message message = new Message(Message.TYPE_CHAT_JOIN); + JsonObject payload = new JsonObject(); + payload.addProperty("userId", userId); + payload.addProperty("name", name); + message.setPayload(payload); + + String json = message.toJson(); + + // Send to all chat clients + for (ClientInfo client : clients.values()) { + if ("chat".equals(client.getClientType()) && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + } + + /** + * Broadcast chat leave notification. + */ + public void broadcastChatLeave(String userId, String name) { + Message message = new Message(Message.TYPE_CHAT_LEAVE); + JsonObject payload = new JsonObject(); + payload.addProperty("userId", userId); + payload.addProperty("name", name); + message.setPayload(payload); + + String json = message.toJson(); + + // Send to all chat clients + for (ClientInfo client : clients.values()) { + if ("chat".equals(client.getClientType()) && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + } + + /** + * Broadcast chat message to all chat clients. + */ + public void broadcastChatMessage(JsonObject msgPayload, Channel senderChannel) { + Message message = new Message(Message.TYPE_CHAT_MESSAGE); + message.setPayload(msgPayload); + + String json = message.toJson(); + + // Check if there are specific recipients + JsonArray recipients = null; + if (msgPayload.has("recipients") && !msgPayload.get("recipients").isJsonNull()) { + recipients = msgPayload.getAsJsonArray("recipients"); + } + + // Send to all chat clients (or specific recipients) + for (ClientInfo client : clients.values()) { + if (!"chat".equals(client.getClientType()) || !client.getChannel().isActive()) { + continue; + } + + // Don't send back to sender + if (client.getChannel() == senderChannel) { + continue; + } + + // If recipients specified, only send to them + if (recipients != null) { + boolean isRecipient = false; + for (int i = 0; i < recipients.size(); i++) { + if (client.getUserId().equals(recipients.get(i).getAsString())) { + isRecipient = true; + break; + } + } + if (!isRecipient) { + continue; + } + } + + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/SslContextFactory.java b/android/app/src/main/java/seven/lab/wstun/server/SslContextFactory.java new file mode 100644 index 0000000..5b71e5b --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/SslContextFactory.java @@ -0,0 +1,146 @@ +package seven.lab.wstun.server; + +import android.content.Context; +import android.util.Log; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.Date; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; + +/** + * Factory for creating SSL context with self-signed certificates. + */ +public class SslContextFactory { + + private static final String TAG = "SslContextFactory"; + private static final String KEYSTORE_FILE = "wstun.keystore"; + private static final String KEYSTORE_PASSWORD = "wstunpass"; + private static final String KEY_ALIAS = "wstun"; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * Get or create SSL context with self-signed certificate. + */ + public static SslContext getSslContext(Context context) throws Exception { + File keystoreFile = new File(context.getFilesDir(), KEYSTORE_FILE); + + KeyStore keyStore; + if (!keystoreFile.exists()) { + Log.i(TAG, "Generating new self-signed certificate"); + keyStore = generateSelfSignedCertificate(); + saveKeyStore(keyStore, keystoreFile); + } else { + Log.i(TAG, "Loading existing certificate"); + keyStore = loadKeyStore(keystoreFile); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, KEYSTORE_PASSWORD.toCharArray()); + + return SslContextBuilder.forServer(kmf).build(); + } + + /** + * Generate a new self-signed certificate. + */ + private static KeyStore generateSelfSignedCertificate() throws Exception { + // Generate key pair + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair keyPair = keyPairGen.generateKeyPair(); + + // Certificate validity period + long now = System.currentTimeMillis(); + Date startDate = new Date(now); + Date endDate = new Date(now + 365L * 24 * 60 * 60 * 1000); // 1 year + + // Build certificate + X500Name issuer = new X500Name("CN=WSTun, O=WSTun, L=Local, C=US"); + BigInteger serial = BigInteger.valueOf(now); + + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + issuer, + serial, + startDate, + endDate, + issuer, + keyPair.getPublic() + ); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA") + .setProvider("BC") + .build(keyPair.getPrivate()); + + X509CertificateHolder certHolder = certBuilder.build(signer); + X509Certificate cert = new JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(certHolder); + + // Create keystore + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + keyStore.setKeyEntry( + KEY_ALIAS, + keyPair.getPrivate(), + KEYSTORE_PASSWORD.toCharArray(), + new X509Certificate[]{cert} + ); + + return keyStore; + } + + /** + * Save keystore to file. + */ + private static void saveKeyStore(KeyStore keyStore, File file) throws Exception { + try (FileOutputStream fos = new FileOutputStream(file)) { + keyStore.store(fos, KEYSTORE_PASSWORD.toCharArray()); + } + } + + /** + * Load keystore from file. + */ + private static KeyStore loadKeyStore(File file) throws Exception { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (FileInputStream fis = new FileInputStream(file)) { + keyStore.load(fis, KEYSTORE_PASSWORD.toCharArray()); + } + return keyStore; + } + + /** + * Delete existing certificate to regenerate. + */ + public static void deleteCertificate(Context context) { + File keystoreFile = new File(context.getFilesDir(), KEYSTORE_FILE); + if (keystoreFile.exists()) { + keystoreFile.delete(); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/WebSocketHandler.java b/android/app/src/main/java/seven/lab/wstun/server/WebSocketHandler.java new file mode 100644 index 0000000..e83c023 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/WebSocketHandler.java @@ -0,0 +1,805 @@ +package seven.lab.wstun.server; + +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import java.util.List; + +import seven.lab.wstun.config.ServerConfig; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import seven.lab.wstun.protocol.HttpRelayResponse; +import seven.lab.wstun.protocol.Message; +import seven.lab.wstun.protocol.ServiceRegistration; + +/** + * Handler for WebSocket connections from service clients. + */ +public class WebSocketHandler extends SimpleChannelInboundHandler { + + private static final String TAG = "WebSocketHandler"; + private static final Gson gson = new Gson(); + + private final ServiceManager serviceManager; + private final RequestManager requestManager; + + public WebSocketHandler(ServiceManager serviceManager, RequestManager requestManager) { + this.serviceManager = serviceManager; + this.requestManager = requestManager; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { + if (frame instanceof TextWebSocketFrame) { + handleTextFrame(ctx, (TextWebSocketFrame) frame); + } else if (frame instanceof BinaryWebSocketFrame) { + handleBinaryFrame(ctx, (BinaryWebSocketFrame) frame); + } else if (frame instanceof PingWebSocketFrame) { + ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain())); + } else if (frame instanceof CloseWebSocketFrame) { + ctx.close(); + } + } + + private void handleTextFrame(ChannelHandlerContext ctx, TextWebSocketFrame frame) { + String text = frame.text(); + + try { + Message message = Message.fromJson(text); + handleMessage(ctx, message); + } catch (Exception e) { + Log.e(TAG, "Failed to parse message", e); + sendError(ctx, "Invalid message format"); + } + } + + private void handleBinaryFrame(ChannelHandlerContext ctx, BinaryWebSocketFrame frame) { + ByteBuf content = frame.content(); + // Binary frames are used for file data streaming + // First byte is message type, rest is data + if (content.readableBytes() < 1) { + return; + } + + byte type = content.readByte(); + byte[] data = new byte[content.readableBytes()]; + content.readBytes(data); + + // Binary data is handled silently for performance + } + + private void handleMessage(ChannelHandlerContext ctx, Message message) { + String type = message.getType(); + + switch (type) { + case Message.TYPE_REGISTER: + handleRegister(ctx, message); + break; + case Message.TYPE_UNREGISTER: + handleUnregister(ctx, message); + break; + case Message.TYPE_CLIENT_REGISTER: + handleClientRegister(ctx, message); + break; + case Message.TYPE_HTTP_RESPONSE: + handleHttpResponse(ctx, message); + break; + case Message.TYPE_HTTP_RESPONSE_START: + handleHttpResponseStart(ctx, message); + break; + case Message.TYPE_HTTP_RESPONSE_CHUNK: + handleHttpResponseChunk(ctx, message); + break; + case Message.TYPE_FILE_REGISTER: + handleFileRegister(ctx, message); + break; + case Message.TYPE_FILE_UNREGISTER: + handleFileUnregister(ctx, message); + break; + case Message.TYPE_CHAT_JOIN: + handleChatJoin(ctx, message); + break; + case Message.TYPE_CHAT_LEAVE: + handleChatLeave(ctx, message); + break; + case Message.TYPE_DATA: + handleData(ctx, message); + break; + case Message.TYPE_KICK_CLIENT: + handleKickClient(ctx, message); + break; + case Message.TYPE_FILE_LIST: + case Message.TYPE_CHAT_USER_LIST: + // These are broadcast messages from service to clients + handleBroadcast(ctx, message); + break; + case Message.TYPE_FILE_REQUEST: + handleFileRequest(ctx, message); + break; + case Message.TYPE_CREATE_INSTANCE: + handleCreateInstance(ctx, message); + break; + case Message.TYPE_JOIN_INSTANCE: + handleJoinInstance(ctx, message); + break; + case Message.TYPE_LIST_INSTANCES: + handleListInstances(ctx, message); + break; + case Message.TYPE_RECLAIM_INSTANCE: + handleReclaimInstance(ctx, message); + break; + // Instance-aware messages for fileshare/chat + case Message.TYPE_USER_JOIN: + case Message.TYPE_USER_LEAVE: + case Message.TYPE_FILE_ADD: + case Message.TYPE_FILE_REMOVE: + case Message.TYPE_REQUEST_STATE: + case Message.TYPE_STATE_RESPONSE: + case Message.TYPE_CHAT_MESSAGE: + handleInstanceMessage(ctx, message); + break; + default: + Log.w(TAG, "Unknown message type: " + type); + } + } + + private void handleRegister(ChannelHandlerContext ctx, Message message) { + try { + ServiceRegistration registration = Message.payloadAs( + message.getPayload(), + ServiceRegistration.class + ); + + boolean success = serviceManager.registerService(registration, ctx.channel()); + + // Send acknowledgment + Message ack = new Message(Message.TYPE_ACK); + ack.setId(message.getId()); + JsonObject payload = new JsonObject(); + payload.addProperty("success", success); + payload.addProperty("message", success ? "Service registered" : "Registration failed"); + ack.setPayload(payload); + + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + } catch (Exception e) { + Log.e(TAG, "Failed to register service", e); + sendError(ctx, "Registration failed: " + e.getMessage()); + } + } + + private void handleUnregister(ChannelHandlerContext ctx, Message message) { + String serviceName = message.getService(); + if (serviceName != null) { + serviceManager.unregisterService(serviceName); + } + + Message ack = new Message(Message.TYPE_ACK); + ack.setId(message.getId()); + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + } + + private void handleHttpResponse(ChannelHandlerContext ctx, Message message) { + try { + HttpRelayResponse response = Message.payloadAs( + message.getPayload(), + HttpRelayResponse.class + ); + + PendingRequest pending = requestManager.removePendingRequest(response.getRequestId()); + if (pending != null) { + HttpHandler.sendRelayResponse(pending.getCtx(), response); + } else { + Log.w(TAG, "No pending request for: " + response.getRequestId()); + } + } catch (Exception e) { + Log.e(TAG, "Failed to handle HTTP response", e); + } + } + + private void handleData(ChannelHandlerContext ctx, Message message) { + // Handle service-specific data messages + String service = message.getService(); + if (service != null) { + ServiceManager.ServiceEntry entry = serviceManager.getService(service); + if (entry != null) { + // Broadcast or route data as needed + Log.d(TAG, "Data message for service: " + service); + } + } + } + + /** + * Handle streaming HTTP response start (headers). + */ + private void handleHttpResponseStart(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String requestId = payload.get("request_id").getAsString(); + int status = payload.get("status").getAsInt(); + JsonObject headers = payload.getAsJsonObject("headers"); + + PendingRequest pending = requestManager.getPendingRequest(requestId); + if (pending != null) { + HttpHandler.startStreamingResponse(pending.getCtx(), requestId, status, headers); + } else { + Log.w(TAG, "No pending request for streaming start: " + requestId); + } + } catch (Exception e) { + Log.e(TAG, "Failed to handle HTTP response start", e); + } + } + + /** + * Handle streaming HTTP response chunk. + */ + private void handleHttpResponseChunk(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String requestId = payload.get("request_id").getAsString(); + boolean done = payload.has("done") && payload.get("done").getAsBoolean(); + + String chunkBase64 = payload.has("chunk_base64") ? + payload.get("chunk_base64").getAsString() : null; + String error = payload.has("error") ? + payload.get("error").getAsString() : null; + + PendingRequest pending = done ? + requestManager.removePendingRequest(requestId) : + requestManager.getPendingRequest(requestId); + + if (pending != null) { + if (error != null) { + HttpHandler.sendStreamingError(pending.getCtx(), error); + } else if (chunkBase64 != null) { + HttpHandler.sendStreamingChunk(pending.getCtx(), chunkBase64); + } + + if (done) { + HttpHandler.endStreamingResponse(pending.getCtx()); + } + } else { + Log.w(TAG, "No pending request for streaming chunk: " + requestId); + } + } catch (Exception e) { + Log.e(TAG, "Failed to handle HTTP response chunk", e); + } + } + + /** + * Handle file registration for relay sharing. + */ + private void handleFileRegister(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String fileId = payload.get("fileId").getAsString(); + String filename = payload.get("filename").getAsString(); + long size = payload.get("size").getAsLong(); + String mimeType = payload.has("mimeType") ? + payload.get("mimeType").getAsString() : "application/octet-stream"; + String ownerId = payload.has("ownerId") ? + payload.get("ownerId").getAsString() : "unknown"; + + serviceManager.registerFile(fileId, filename, size, mimeType, ownerId, ctx.channel()); + } catch (Exception e) { + Log.e(TAG, "Failed to register file", e); + } + } + + /** + * Handle file unregistration. + */ + private void handleFileUnregister(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String fileId = payload.get("fileId").getAsString(); + serviceManager.unregisterFile(fileId); + } catch (Exception e) { + Log.e(TAG, "Failed to unregister file", e); + } + } + + /** + * Handle client registration (for user clients, not services). + * Clients can connect to share files or chat without taking over the service. + */ + private void handleClientRegister(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + // Use service field first, then payload.clientType as fallback + String clientType = message.getService(); + if (clientType == null || clientType.isEmpty()) { + clientType = payload.has("clientType") ? payload.get("clientType").getAsString() : "unknown"; + } + String userId = payload.has("userId") ? payload.get("userId").getAsString() : "u" + System.currentTimeMillis(); + + // Validate service auth token if service requires it + ServiceManager.ServiceEntry service = serviceManager.getService(clientType); + if (service != null && service.hasAuth()) { + String clientToken = payload.has("auth_token") ? payload.get("auth_token").getAsString() : null; + if (!service.validateClientAuth(clientToken)) { + sendError(ctx, "Invalid service auth token"); + return; + } + } + + // Register the client with the service manager + serviceManager.registerClient(userId, clientType, ctx.channel()); + + // Send acknowledgment + Message ack = new Message(Message.TYPE_ACK); + ack.setId(message.getId()); + JsonObject ackPayload = new JsonObject(); + ackPayload.addProperty("success", true); + ackPayload.addProperty("message", "Client registered"); + ackPayload.addProperty("userId", userId); + ack.setPayload(ackPayload); + + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + + // Send current file list if fileshare client + if ("fileshare".equals(clientType)) { + serviceManager.broadcastFileList(); + } + + // Send current user list if chat client + if ("chat".equals(clientType)) { + serviceManager.broadcastChatUserList(); + } + + Log.i(TAG, "Client registered: " + userId + " (type: " + clientType + ")"); + } catch (Exception e) { + Log.e(TAG, "Failed to register client", e); + sendError(ctx, "Client registration failed: " + e.getMessage()); + } + } + + /** + * Handle chat join message. + */ + private void handleChatJoin(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String userId = payload.get("userId").getAsString(); + String name = payload.has("name") ? payload.get("name").getAsString() : "Anonymous"; + + // Register chat user + serviceManager.registerChatUser(userId, name, ctx.channel()); + + // Broadcast to all chat clients + serviceManager.broadcastChatJoin(userId, name); + serviceManager.broadcastChatUserList(); + + Log.i(TAG, "Chat user joined: " + name + " (" + userId + ")"); + } catch (Exception e) { + Log.e(TAG, "Failed to handle chat join", e); + } + } + + /** + * Handle chat leave message. + */ + private void handleChatLeave(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String userId = payload.get("userId").getAsString(); + + String name = serviceManager.getChatUserName(userId); + serviceManager.unregisterChatUser(userId); + + // Broadcast to all chat clients + if (name != null) { + serviceManager.broadcastChatLeave(userId, name); + serviceManager.broadcastChatUserList(); + } + + Log.i(TAG, "Chat user left: " + userId); + } catch (Exception e) { + Log.e(TAG, "Failed to handle chat leave", e); + } + } + + /** + * Handle chat message. + */ + private void handleChatMessage(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + + // Broadcast to all chat clients (or specific recipients if specified) + serviceManager.broadcastChatMessage(payload, ctx.channel()); + + } catch (Exception e) { + Log.e(TAG, "Failed to handle chat message", e); + } + } + + /** + * Handle kick client request from service. + */ + private void handleKickClient(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String userId = payload.get("userId").getAsString(); + serviceManager.kickClient(userId); + } catch (Exception e) { + Log.e(TAG, "Failed to kick client", e); + } + } + + /** + * Handle create instance request from service client. + */ + private void handleCreateInstance(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + if (payload == null) { + payload = new JsonObject(); + } + + String serviceName = message.getService(); + if (serviceName == null || serviceName.isEmpty()) { + sendError(ctx, "Service name is required"); + return; + } + + String name = getStringFromPayload(payload, "name", "Room"); + String token = getStringFromPayload(payload, "token", null); + + // Validate server auth + ServerConfig config = HttpHandler.getServerConfig(); + if (config != null && config.isAuthEnabled()) { + String authToken = getStringFromPayload(payload, "server_token", null); + if (!config.validateServerAuth(authToken)) { + sendError(ctx, "Invalid server auth token"); + return; + } + } + + ServiceManager.ServiceInstance instance = serviceManager.createInstance( + serviceName, name, token, ctx.channel() + ); + + // Send success response + Message ack = new Message(Message.TYPE_INSTANCE_CREATED); + ack.setService(serviceName); + JsonObject ackPayload = new JsonObject(); + ackPayload.addProperty("success", true); + ackPayload.addProperty("uuid", instance.getUuid()); + ackPayload.addProperty("name", instance.getName()); + ackPayload.addProperty("hasToken", instance.hasToken()); + ack.setPayload(ackPayload); + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + + Log.i(TAG, "Instance created: " + instance.getUuid() + " (" + name + ")"); + } catch (Exception e) { + Log.e(TAG, "Failed to create instance", e); + sendError(ctx, "Failed to create instance: " + e.getMessage()); + } + } + + /** + * Handle join instance request from user client. + */ + private void handleJoinInstance(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + if (payload == null) { + sendError(ctx, "Missing payload"); + return; + } + + String serviceName = message.getService(); + String instanceUuid = getStringFromPayload(payload, "uuid", null); + if (instanceUuid == null || instanceUuid.isEmpty()) { + sendError(ctx, "Instance UUID is required"); + return; + } + String userId = getStringFromPayload(payload, "userId", "u" + System.currentTimeMillis()); + String token = getStringFromPayload(payload, "token", null); + + // Find the instance + ServiceManager.ServiceInstance instance = serviceManager.getInstance(instanceUuid); + if (instance == null || !instance.isOwnerConnected()) { + sendError(ctx, "Instance not found or not available"); + return; + } + + // Validate instance token + if (!instance.validateToken(token)) { + sendError(ctx, "Invalid instance token"); + return; + } + + // Register client to instance + serviceManager.registerClient(userId, serviceName, instanceUuid, ctx.channel()); + + // Send success response + Message ack = new Message(Message.TYPE_ACK); + ack.setService(serviceName); + JsonObject ackPayload = new JsonObject(); + ackPayload.addProperty("success", true); + ackPayload.addProperty("message", "Joined instance"); + ackPayload.addProperty("userId", userId); + ackPayload.addProperty("instanceUuid", instanceUuid); + ackPayload.addProperty("instanceName", instance.getName()); + ack.setPayload(ackPayload); + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + + Log.i(TAG, "Client " + userId + " joined instance " + instanceUuid); + } catch (Exception e) { + Log.e(TAG, "Failed to join instance", e); + sendError(ctx, "Failed to join instance: " + e.getMessage()); + } + } + + /** + * Handle list instances request. + */ + private void handleListInstances(ChannelHandlerContext ctx, Message message) { + try { + String serviceName = message.getService(); + + List instances = serviceManager.getInstancesForService(serviceName); + + Message response = new Message(Message.TYPE_INSTANCE_LIST); + response.setService(serviceName); + JsonObject payload = new JsonObject(); + JsonArray arr = new JsonArray(); + for (ServiceManager.ServiceInstance inst : instances) { + arr.add(inst.toJson()); + } + payload.add("instances", arr); + response.setPayload(payload); + + ctx.writeAndFlush(new TextWebSocketFrame(response.toJson())); + } catch (Exception e) { + Log.e(TAG, "Failed to list instances", e); + sendError(ctx, "Failed to list instances: " + e.getMessage()); + } + } + + /** + * Handle reclaim instance request - allows owner to reconnect to their instance. + */ + private void handleReclaimInstance(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + if (payload == null) { + sendError(ctx, "Missing payload"); + return; + } + + String serviceName = message.getService(); + String uuid = getStringFromPayload(payload, "uuid", null); + String token = getStringFromPayload(payload, "token", null); + + if (uuid == null || uuid.isEmpty()) { + sendError(ctx, "Instance UUID is required"); + return; + } + + // Validate server auth + ServerConfig config = HttpHandler.getServerConfig(); + if (config != null && config.isAuthEnabled()) { + String authToken = getStringFromPayload(payload, "server_token", null); + if (!config.validateServerAuth(authToken)) { + sendError(ctx, "Invalid server auth token"); + return; + } + } + + // Find the instance and validate token + ServiceManager.ServiceInstance instance = serviceManager.getInstance(uuid); + if (instance == null) { + sendError(ctx, "Instance not found"); + return; + } + + // Validate instance token + if (!instance.validateToken(token)) { + sendError(ctx, "Invalid instance token"); + return; + } + + // Try to reclaim + boolean success = serviceManager.reclaimInstance(uuid, ctx.channel()); + + Message response = new Message(Message.TYPE_INSTANCE_RECLAIMED); + response.setService(serviceName); + JsonObject respPayload = new JsonObject(); + respPayload.addProperty("success", success); + if (success) { + respPayload.addProperty("uuid", uuid); + respPayload.addProperty("name", instance.getName()); + respPayload.addProperty("hasToken", instance.hasToken()); + respPayload.addProperty("userCount", instance.getUserCount()); + } else { + respPayload.addProperty("error", "Cannot reclaim instance (owner already connected or grace period expired)"); + } + response.setPayload(respPayload); + + ctx.writeAndFlush(new TextWebSocketFrame(response.toJson())); + + if (success) { + Log.i(TAG, "Instance reclaimed: " + uuid); + } + } catch (Exception e) { + Log.e(TAG, "Failed to reclaim instance", e); + sendError(ctx, "Failed to reclaim instance: " + e.getMessage()); + } + } + + /** + * Handle broadcast messages from service to all clients. + */ + private void handleBroadcast(ChannelHandlerContext ctx, Message message) { + try { + String service = message.getService(); + if (service == null) return; + + String json = message.toJson(); + + // Send to all clients of this service type + for (ServiceManager.ClientInfo client : serviceManager.getClientsByType(service)) { + if (client.getChannel() != null && client.getChannel().isActive() && client.getChannel() != ctx.channel()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to broadcast message", e); + } + } + + /** + * Handle file request from service to specific client. + */ + private void handleFileRequest(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String requestId = payload.get("requestId").getAsString(); + String fileId = payload.get("fileId").getAsString(); + String ownerId = payload.has("ownerId") ? payload.get("ownerId").getAsString() : null; + + // Find the file owner and send the request + if (ownerId != null) { + ServiceManager.ClientInfo owner = serviceManager.getClient(ownerId); + if (owner != null && owner.getChannel() != null && owner.getChannel().isActive()) { + // Forward the request to the owner + Message request = new Message(Message.TYPE_FILE_REQUEST); + request.setService(message.getService()); + JsonObject reqPayload = new JsonObject(); + reqPayload.addProperty("requestId", requestId); + reqPayload.addProperty("fileId", fileId); + request.setPayload(reqPayload); + owner.getChannel().writeAndFlush(new TextWebSocketFrame(request.toJson())); + return; + } + } + + // Owner not found + Log.w(TAG, "File owner not found: " + ownerId); + } catch (Exception e) { + Log.e(TAG, "Failed to handle file request", e); + } + } + + /** + * Handle instance-specific messages (user_join, file_add, etc.) + * These messages are broadcast to all other users in the same instance. + */ + private void handleInstanceMessage(ChannelHandlerContext ctx, Message message) { + try { + // Find which client/instance this message is from + ServiceManager.ClientInfo sender = serviceManager.getClientByChannel(ctx.channel()); + if (sender == null) { + Log.w(TAG, "Instance message from unknown client"); + return; + } + + String instanceUuid = sender.getInstanceUuid(); + String senderUserId = sender.getUserId(); + + // Broadcast to all other users in the same instance + String json = message.toJson(); + List instanceClients; + + if (instanceUuid != null) { + instanceClients = serviceManager.getClientsInInstance(instanceUuid); + } else { + // Fallback to service-based broadcasting for legacy clients + instanceClients = serviceManager.getClientsByType(sender.getClientType()); + } + + for (ServiceManager.ClientInfo client : instanceClients) { + // Don't send back to sender + if (client.getUserId().equals(senderUserId)) { + continue; + } + + if (client.getChannel() != null && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + + // Also send to the instance owner if any + if (instanceUuid != null) { + ServiceManager.ServiceInstance instance = serviceManager.getInstance(instanceUuid); + if (instance != null && instance.getOwnerChannel() != null && + instance.getOwnerChannel().isActive() && + instance.getOwnerChannel() != ctx.channel()) { + instance.getOwnerChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + + Log.d(TAG, "Broadcast instance message: " + message.getType() + " to " + instanceClients.size() + " clients"); + } catch (Exception e) { + Log.e(TAG, "Failed to handle instance message", e); + } + } + + /** + * Safely get a string from JsonObject, handling null and JsonNull. + */ + private String getStringFromPayload(JsonObject payload, String key, String defaultValue) { + if (payload == null || !payload.has(key)) { + return defaultValue; + } + try { + if (payload.get(key).isJsonNull()) { + return defaultValue; + } + return payload.get(key).getAsString(); + } catch (Exception e) { + return defaultValue; + } + } + + private void sendError(ChannelHandlerContext ctx, String error) { + Message errorMsg = new Message(Message.TYPE_ERROR); + JsonObject payload = new JsonObject(); + payload.addProperty("message", error); // Client expects 'message' field + payload.addProperty("error", error); // Also include 'error' for compatibility + errorMsg.setPayload(payload); + ctx.writeAndFlush(new TextWebSocketFrame(errorMsg.toJson())); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + Log.i(TAG, "WebSocket channel disconnected"); + + // Clean up any pending requests for this service + cleanupPendingRequestsForChannel(ctx); + + serviceManager.onChannelDisconnect(ctx.channel()); + super.channelInactive(ctx); + } + + /** + * Clean up pending HTTP requests when a service client disconnects. + * This sends error responses to HTTP clients waiting for relay responses. + */ + private void cleanupPendingRequestsForChannel(ChannelHandlerContext ctx) { + // Find and fail all pending requests that were being handled by this channel + // The RequestManager should have a method to get requests by service + // For now, we rely on the timeout mechanism, but we can improve this + Log.d(TAG, "Cleaning up pending requests for disconnected channel"); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + Log.e(TAG, "WebSocket error", cause); + ctx.close(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/service/WSTunService.java b/android/app/src/main/java/seven/lab/wstun/service/WSTunService.java new file mode 100644 index 0000000..6e0a83a --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/service/WSTunService.java @@ -0,0 +1,247 @@ +package seven.lab.wstun.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +import seven.lab.wstun.R; +import seven.lab.wstun.config.ServerConfig; +import seven.lab.wstun.server.LocalServiceManager; +import seven.lab.wstun.server.NettyServer; +import seven.lab.wstun.server.ServiceManager; +import seven.lab.wstun.ui.MainActivity; + +/** + * Foreground service that runs the HTTP/WebSocket server. + */ +public class WSTunService extends Service { + + private static final String TAG = "WSTunService"; + private static final String CHANNEL_ID = "wstun_service"; + private static final int NOTIFICATION_ID = 1; + + public static final String ACTION_START = "seven.lab.wstun.START"; + public static final String ACTION_STOP = "seven.lab.wstun.STOP"; + + private final IBinder binder = new LocalBinder(); + private NettyServer server; + private ServerConfig config; + private ServiceManager serviceManager; + private boolean isRunning = false; + + private ServiceListener listener; + + public interface ServiceListener { + void onServerStarted(); + void onServerStopped(); + void onServiceChanged(); + void onError(String message); + } + + public class LocalBinder extends Binder { + public WSTunService getService() { + return WSTunService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + config = new ServerConfig(this); + serviceManager = new ServiceManager(); + serviceManager.setListener(new ServiceManager.ServiceChangeListener() { + @Override + public void onServiceAdded(ServiceManager.ServiceEntry service) { + if (listener != null) { + listener.onServiceChanged(); + } + } + + @Override + public void onServiceRemoved(ServiceManager.ServiceEntry service) { + if (listener != null) { + listener.onServiceChanged(); + } + } + }); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + String action = intent.getAction(); + if (ACTION_START.equals(action)) { + startServer(); + } else if (ACTION_STOP.equals(action)) { + stopServer(); + stopSelf(); + } + } + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public void onDestroy() { + stopServer(); + super.onDestroy(); + } + + /** + * Start the HTTP server. + */ + public void startServer() { + if (isRunning) { + Log.w(TAG, "Server already running"); + return; + } + + try { + server = new NettyServer(this, config, serviceManager); + server.start(); + isRunning = true; + + // Start foreground + startForeground(NOTIFICATION_ID, createNotification()); + + if (listener != null) { + listener.onServerStarted(); + } + + Log.i(TAG, "Server started on port " + config.getPort()); + } catch (Exception e) { + Log.e(TAG, "Failed to start server", e); + if (listener != null) { + listener.onError("Failed to start server: " + e.getMessage()); + } + } + } + + /** + * Stop the HTTP server. + */ + public void stopServer() { + if (!isRunning) { + return; + } + + if (server != null) { + server.stop(); + server = null; + } + + isRunning = false; + stopForeground(true); + + if (listener != null) { + listener.onServerStopped(); + } + + Log.i(TAG, "Server stopped"); + } + + /** + * Check if server is running. + */ + public boolean isServerRunning() { + return isRunning; + } + + /** + * Get server port. + */ + public int getPort() { + return config.getPort(); + } + + /** + * Check if HTTPS is enabled. + */ + public boolean isHttpsEnabled() { + return config.isHttpsEnabled(); + } + + /** + * Get service manager. + */ + public ServiceManager getServiceManager() { + return serviceManager; + } + + /** + * Set service listener. + */ + public void setListener(ServiceListener listener) { + this.listener = listener; + } + + /** + * Kick a service by name. + */ + public void kickService(String serviceName) { + if (serviceManager != null) { + serviceManager.kickService(serviceName); + } + } + + /** + * Get the local service manager. + */ + public LocalServiceManager getLocalServiceManager() { + return server != null ? server.getLocalServiceManager() : null; + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription(getString(R.string.notification_channel_desc)); + + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } + + private Notification createNotification() { + Intent intent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, 0, intent, PendingIntent.FLAG_IMMUTABLE + ); + + Intent stopIntent = new Intent(this, WSTunService.class); + stopIntent.setAction(ACTION_STOP); + PendingIntent stopPendingIntent = PendingIntent.getService( + this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE + ); + + String protocol = config.isHttpsEnabled() ? "HTTPS" : "HTTP"; + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.notification_title)) + .setContentText(protocol + " server running on port " + config.getPort()) + .setSmallIcon(android.R.drawable.ic_menu_share) + .setContentIntent(pendingIntent) + .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent) + .setOngoing(true) + .build(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/ConfigFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/ConfigFragment.java new file mode 100644 index 0000000..639c741 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/ConfigFragment.java @@ -0,0 +1,341 @@ +package seven.lab.wstun.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.material.textfield.TextInputEditText; + +import seven.lab.wstun.R; +import seven.lab.wstun.config.ServerConfig; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment for server configuration. + */ +public class ConfigFragment extends Fragment { + + private WSTunService service; + private ServerConfig config; + + private TextInputEditText portInput; + private TextInputEditText corsOriginsInput; + private CheckBox httpsCheckbox; + private TextView certInfo; + private Button saveButton; + private CheckBox debugLogsCheckbox; + private TextView debugLogsUrl; + private Button viewLogsButton; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_config, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + config = new ServerConfig(requireContext()); + + portInput = view.findViewById(R.id.portInput); + corsOriginsInput = view.findViewById(R.id.corsOriginsInput); + httpsCheckbox = view.findViewById(R.id.httpsCheckbox); + certInfo = view.findViewById(R.id.certInfo); + saveButton = view.findViewById(R.id.saveButton); + debugLogsCheckbox = view.findViewById(R.id.debugLogsCheckbox); + debugLogsUrl = view.findViewById(R.id.debugLogsUrl); + viewLogsButton = view.findViewById(R.id.viewLogsButton); + + loadConfig(); + + httpsCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + certInfo.setVisibility(isChecked ? View.VISIBLE : View.GONE); + }); + + debugLogsCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + config.setDebugLogsEnabled(isChecked); + updateDebugLogsUrl(); + }); + + saveButton.setOnClickListener(v -> saveConfig()); + viewLogsButton.setOnClickListener(v -> showLogViewer()); + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + updateUI(); + } + + private void loadConfig() { + portInput.setText(String.valueOf(config.getPort())); + httpsCheckbox.setChecked(config.isHttpsEnabled()); + certInfo.setVisibility(config.isHttpsEnabled() ? View.VISIBLE : View.GONE); + corsOriginsInput.setText(config.getCorsOrigins()); + debugLogsCheckbox.setChecked(config.isDebugLogsEnabled()); + updateDebugLogsUrl(); + } + + private void updateDebugLogsUrl() { + if (config.isDebugLogsEnabled()) { + String protocol = config.isHttpsEnabled() ? "https" : "http"; + String url = protocol + "://[device-ip]:" + config.getPort() + "/debug/logs"; + debugLogsUrl.setText("Access " + url + " in browser to view live server logs"); + } else { + debugLogsUrl.setText("Enable to access /debug/logs endpoint for live server logs"); + } + } + + private void saveConfig() { + // Validate port + String portStr = portInput.getText().toString().trim(); + int port; + try { + port = Integer.parseInt(portStr); + if (port < 1 || port > 65535) { + Toast.makeText(getContext(), "Port must be between 1 and 65535", Toast.LENGTH_SHORT).show(); + return; + } + } catch (NumberFormatException e) { + Toast.makeText(getContext(), "Invalid port number", Toast.LENGTH_SHORT).show(); + return; + } + + // Check if server is running + if (service != null && service.isServerRunning()) { + Toast.makeText(getContext(), "Stop server before changing configuration", Toast.LENGTH_SHORT).show(); + return; + } + + // Validate CORS origins + String corsOrigins = corsOriginsInput.getText().toString().trim(); + if (corsOrigins.isEmpty()) { + corsOrigins = "*"; + } + + // Save config + config.setPort(port); + config.setHttpsEnabled(httpsCheckbox.isChecked()); + config.setCorsOrigins(corsOrigins); + + Toast.makeText(getContext(), "Configuration saved", Toast.LENGTH_SHORT).show(); + } + + private void updateUI() { + if (getActivity() == null || !isAdded()) return; + + requireActivity().runOnUiThread(() -> { + boolean canEdit = service == null || !service.isServerRunning(); + portInput.setEnabled(canEdit); + httpsCheckbox.setEnabled(canEdit); + corsOriginsInput.setEnabled(canEdit); + saveButton.setEnabled(canEdit); + + updateDebugLogsUrl(); + }); + } + + // Log viewer thread and process + private Thread logViewerThread; + private Process logViewerProcess; + private volatile boolean logViewerRunning = false; + + // Track if user has scrolled manually (disable auto-scroll) + private volatile boolean userScrolling = false; + private String logFilter = ""; + + private void showLogViewer() { + // Create a dialog with a scrollable TextView for logs + android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(requireContext()); + builder.setTitle("Live Logs"); + + // Create main layout + android.widget.LinearLayout mainLayout = new android.widget.LinearLayout(requireContext()); + mainLayout.setOrientation(android.widget.LinearLayout.VERTICAL); + mainLayout.setPadding(16, 16, 16, 16); + + // Create filter input + android.widget.LinearLayout filterLayout = new android.widget.LinearLayout(requireContext()); + filterLayout.setOrientation(android.widget.LinearLayout.HORIZONTAL); + filterLayout.setGravity(android.view.Gravity.CENTER_VERTICAL); + + final android.widget.EditText filterInput = new android.widget.EditText(requireContext()); + filterInput.setHint("Filter keywords..."); + filterInput.setSingleLine(true); + filterInput.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + 0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1)); + + final android.widget.CheckBox autoScrollCheckbox = new android.widget.CheckBox(requireContext()); + autoScrollCheckbox.setText("Auto-scroll"); + autoScrollCheckbox.setChecked(true); + + filterLayout.addView(filterInput); + filterLayout.addView(autoScrollCheckbox); + mainLayout.addView(filterLayout); + + // Create a scrollable TextView + final android.widget.ScrollView scrollView = new android.widget.ScrollView(requireContext()); + scrollView.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 600)); + + final TextView logTextView = new TextView(requireContext()); + logTextView.setTypeface(android.graphics.Typeface.MONOSPACE); + logTextView.setTextSize(10); + logTextView.setPadding(8, 8, 8, 8); + logTextView.setText("Starting log capture...\n"); + logTextView.setTextIsSelectable(true); + scrollView.addView(logTextView); + mainLayout.addView(scrollView); + + // Handle filter changes + filterInput.addTextChangedListener(new android.text.TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + @Override + public void afterTextChanged(android.text.Editable s) { + logFilter = s.toString().toLowerCase(); + } + }); + + // Handle auto-scroll toggle + autoScrollCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + userScrolling = !isChecked; + }); + + // Detect manual scroll + scrollView.setOnTouchListener((v, event) -> { + if (event.getAction() == android.view.MotionEvent.ACTION_DOWN || + event.getAction() == android.view.MotionEvent.ACTION_MOVE) { + userScrolling = true; + autoScrollCheckbox.setChecked(false); + } + return false; + }); + + builder.setView(mainLayout); + builder.setNeutralButton("Clear", null); // Will set listener after show() + builder.setNegativeButton("Close", (dialog, which) -> { + stopLogViewer(); + dialog.dismiss(); + }); + + android.app.AlertDialog dialog = builder.create(); + dialog.setOnDismissListener(d -> stopLogViewer()); + dialog.show(); + + // Set clear button to not dismiss dialog + dialog.getButton(android.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + logTextView.setText(""); + }); + + // Start log capture thread + userScrolling = false; + logFilter = ""; + logViewerRunning = true; + logViewerThread = new Thread(() -> { + java.io.BufferedReader reader = null; + try { + // Clear logcat first and then start capturing + Runtime.getRuntime().exec("logcat -c").waitFor(); + logViewerProcess = Runtime.getRuntime().exec("logcat -v time *:D"); + reader = new java.io.BufferedReader( + new java.io.InputStreamReader(logViewerProcess.getInputStream())); + + String line; + final StringBuilder buffer = new StringBuilder(); + int lineCount = 0; + + while (logViewerRunning && (line = reader.readLine()) != null) { + // Apply filter + String currentFilter = logFilter; + if (currentFilter.isEmpty() || line.toLowerCase().contains(currentFilter)) { + buffer.append(line).append("\n"); + lineCount++; + } + + // Update UI every 5 lines + if (lineCount % 5 == 0 && buffer.length() > 0) { + final String newText = buffer.toString(); + buffer.setLength(0); + + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + // Save scroll position before update if not auto-scrolling + final int scrollY = scrollView.getScrollY(); + + logTextView.append(newText); + + // Limit text length to prevent memory issues + if (logTextView.getText().length() > 50000) { + String text = logTextView.getText().toString(); + logTextView.setText(text.substring(text.length() - 40000)); + } + + // Handle scroll position + if (userScrolling) { + // Restore scroll position when user is reading + scrollView.post(() -> scrollView.scrollTo(0, scrollY)); + } else { + // Auto-scroll to bottom + scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); + } + }); + } + } + } + + // Flush remaining buffer + if (buffer.length() > 0 && getActivity() != null) { + final String remaining = buffer.toString(); + getActivity().runOnUiThread(() -> logTextView.append(remaining)); + } + + } catch (Exception e) { + if (getActivity() != null) { + final String error = "Error: " + e.getMessage() + "\n"; + getActivity().runOnUiThread(() -> logTextView.append(error)); + } + } finally { + if (reader != null) { + try { reader.close(); } catch (Exception ignored) {} + } + if (logViewerProcess != null) { + logViewerProcess.destroy(); + } + } + }); + logViewerThread.setName("LogViewer"); + logViewerThread.start(); + } + + private void stopLogViewer() { + logViewerRunning = false; + if (logViewerProcess != null) { + logViewerProcess.destroy(); + logViewerProcess = null; + } + if (logViewerThread != null) { + logViewerThread.interrupt(); + logViewerThread = null; + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + stopLogViewer(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/InstalledServiceAdapter.java b/android/app/src/main/java/seven/lab/wstun/ui/InstalledServiceAdapter.java new file mode 100644 index 0000000..9bd8ae5 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/InstalledServiceAdapter.java @@ -0,0 +1,140 @@ +package seven.lab.wstun.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import seven.lab.wstun.R; + +/** + * Adapter for displaying installed services. + */ +public class InstalledServiceAdapter extends RecyclerView.Adapter { + + private List services = new ArrayList<>(); + private final ServiceActionListener listener; + + public interface ServiceActionListener { + void onEnableDisable(String serviceName, boolean enable); + void onUninstall(String serviceName); + } + + public static class ServiceItem { + public final String name; + public final String displayName; + public final String description; + public final boolean enabled; + public final boolean builtin; + public final int instanceCount; + + public ServiceItem(String name, String displayName, String description, + boolean enabled, boolean builtin, int instanceCount) { + this.name = name; + this.displayName = displayName != null ? displayName : name; + this.description = description; + this.enabled = enabled; + this.builtin = builtin; + this.instanceCount = instanceCount; + } + } + + public InstalledServiceAdapter(ServiceActionListener listener) { + this.listener = listener; + } + + public void setServices(List services) { + this.services = services; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_installed_service, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ServiceItem item = services.get(position); + holder.bind(item, listener); + } + + @Override + public int getItemCount() { + return services.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView nameText; + private final TextView descText; + private final TextView statusText; + private final TextView instancesText; + private final Button enableButton; + private final Button uninstallButton; + + ViewHolder(View itemView) { + super(itemView); + nameText = itemView.findViewById(R.id.serviceNameText); + descText = itemView.findViewById(R.id.serviceDescText); + statusText = itemView.findViewById(R.id.statusText); + instancesText = itemView.findViewById(R.id.instancesText); + enableButton = itemView.findViewById(R.id.enableButton); + uninstallButton = itemView.findViewById(R.id.uninstallButton); + } + + void bind(ServiceItem item, ServiceActionListener listener) { + nameText.setText(item.displayName); + + if (item.description != null && !item.description.isEmpty()) { + descText.setText(item.description); + descText.setVisibility(View.VISIBLE); + } else { + descText.setVisibility(View.GONE); + } + + if (item.enabled) { + statusText.setText("Enabled"); + statusText.setTextColor(0xFF4CAF50); + enableButton.setText("Disable"); + } else { + statusText.setText("Disabled"); + statusText.setTextColor(0xFF9E9E9E); + enableButton.setText("Enable"); + } + + if (item.instanceCount > 0) { + instancesText.setText(item.instanceCount + " instance(s)"); + instancesText.setVisibility(View.VISIBLE); + } else { + instancesText.setVisibility(View.GONE); + } + + enableButton.setOnClickListener(v -> { + if (listener != null) { + listener.onEnableDisable(item.name, !item.enabled); + } + }); + + if (item.builtin) { + uninstallButton.setVisibility(View.GONE); + } else { + uninstallButton.setVisibility(View.VISIBLE); + uninstallButton.setOnClickListener(v -> { + if (listener != null) { + listener.onUninstall(item.name); + } + }); + } + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/InstalledServicesFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/InstalledServicesFragment.java new file mode 100644 index 0000000..4566fce --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/InstalledServicesFragment.java @@ -0,0 +1,182 @@ +package seven.lab.wstun.ui; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.textfield.TextInputEditText; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import seven.lab.wstun.R; +import seven.lab.wstun.marketplace.InstalledService; +import seven.lab.wstun.server.LocalServiceManager; +import seven.lab.wstun.server.ServiceManager; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment showing installed services. + */ +public class InstalledServicesFragment extends Fragment { + + private WSTunService service; + + private RecyclerView servicesRecyclerView; + private TextView emptyText; + private TextInputEditText filterInput; + + private InstalledServiceAdapter adapter; + private List allItems = new ArrayList<>(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_installed_services, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + servicesRecyclerView = view.findViewById(R.id.servicesRecyclerView); + emptyText = view.findViewById(R.id.emptyText); + filterInput = view.findViewById(R.id.filterInput); + + servicesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new InstalledServiceAdapter(new InstalledServiceAdapter.ServiceActionListener() { + @Override + public void onEnableDisable(String serviceName, boolean enable) { + if (service == null || service.getLocalServiceManager() == null) return; + + if (enable) { + service.getLocalServiceManager().enableService(serviceName); + } else { + service.getLocalServiceManager().disableService(serviceName, service.getServiceManager()); + } + refreshList(); + } + + @Override + public void onUninstall(String serviceName) { + if (service == null || service.getLocalServiceManager() == null) return; + + InstalledService svc = service.getLocalServiceManager().getInstalledService(serviceName); + if (svc != null && "builtin".equals(svc.getSource())) { + Toast.makeText(getContext(), "Cannot uninstall built-in service", Toast.LENGTH_SHORT).show(); + return; + } + + service.getLocalServiceManager().uninstallService(serviceName, service.getServiceManager()); + refreshList(); + Toast.makeText(getContext(), "Service uninstalled", Toast.LENGTH_SHORT).show(); + } + }); + servicesRecyclerView.setAdapter(adapter); + + filterInput.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} + @Override + public void afterTextChanged(Editable s) { + filterServices(s.toString()); + } + }); + + refreshList(); + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + refreshList(); + } + + @Override + public void onResume() { + super.onResume(); + refreshList(); + } + + private void filterServices(String query) { + if (query == null || query.isEmpty()) { + adapter.setServices(allItems); + } else { + String lowerQuery = query.toLowerCase(); + List filtered = new ArrayList<>(); + for (InstalledServiceAdapter.ServiceItem item : allItems) { + if (item.displayName.toLowerCase().contains(lowerQuery) || + item.name.toLowerCase().contains(lowerQuery)) { + filtered.add(item); + } + } + adapter.setServices(filtered); + } + updateEmptyState(); + } + + private void updateEmptyState() { + if (adapter.getItemCount() == 0) { + emptyText.setVisibility(View.VISIBLE); + servicesRecyclerView.setVisibility(View.GONE); + } else { + emptyText.setVisibility(View.GONE); + servicesRecyclerView.setVisibility(View.VISIBLE); + } + } + + private void refreshList() { + if (getActivity() == null || !isAdded()) return; + + requireActivity().runOnUiThread(() -> { + if (service == null || service.getLocalServiceManager() == null) { + allItems.clear(); + adapter.setServices(allItems); + emptyText.setVisibility(View.VISIBLE); + servicesRecyclerView.setVisibility(View.GONE); + return; + } + + LocalServiceManager lsm = service.getLocalServiceManager(); + ServiceManager sm = service.getServiceManager(); + + allItems.clear(); + Map installed = lsm.getInstalledServices(); + + for (Map.Entry entry : installed.entrySet()) { + String name = entry.getKey(); + InstalledService svc = entry.getValue(); + + int instanceCount = 0; + if (sm != null) { + instanceCount = sm.getInstanceCountForService(name); + } + + allItems.add(new InstalledServiceAdapter.ServiceItem( + name, + svc.getDisplayName(), + svc.getManifest() != null ? svc.getManifest().getDescription() : null, + svc.isEnabled(), + "builtin".equals(svc.getSource()), + instanceCount + )); + } + + // Apply current filter + String filter = filterInput.getText() != null ? filterInput.getText().toString() : ""; + filterServices(filter); + }); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/InstanceAdapter.java b/android/app/src/main/java/seven/lab/wstun/ui/InstanceAdapter.java new file mode 100644 index 0000000..8119a31 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/InstanceAdapter.java @@ -0,0 +1,107 @@ +package seven.lab.wstun.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import seven.lab.wstun.R; + +/** + * Adapter for displaying running instances (rooms). + */ +public class InstanceAdapter extends RecyclerView.Adapter { + + private List instances = new ArrayList<>(); + private final InstanceActionListener listener; + + public interface InstanceActionListener { + void onDestroy(InstanceItem instance); + } + + public static class InstanceItem { + public final String uuid; + public final String name; + public final String serviceName; + public final String serviceDisplayName; + public final int userCount; + + public InstanceItem(String uuid, String name, String serviceName, + String serviceDisplayName, int userCount) { + this.uuid = uuid; + this.name = name; + this.serviceName = serviceName; + this.serviceDisplayName = serviceDisplayName; + this.userCount = userCount; + } + + public String getUuid() { + return uuid; + } + } + + public InstanceAdapter(InstanceActionListener listener) { + this.listener = listener; + } + + public void setInstances(List instances) { + this.instances = instances; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_instance, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + InstanceItem item = instances.get(position); + holder.bind(item, listener); + } + + @Override + public int getItemCount() { + return instances.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView nameText; + private final TextView serviceText; + private final TextView usersText; + private final TextView uuidText; + private final Button destroyButton; + + ViewHolder(View itemView) { + super(itemView); + nameText = itemView.findViewById(R.id.instanceNameText); + serviceText = itemView.findViewById(R.id.serviceNameText); + usersText = itemView.findViewById(R.id.usersText); + uuidText = itemView.findViewById(R.id.uuidText); + destroyButton = itemView.findViewById(R.id.destroyButton); + } + + void bind(InstanceItem item, InstanceActionListener listener) { + nameText.setText(item.name); + serviceText.setText(item.serviceDisplayName); + usersText.setText(item.userCount + " user(s)"); + uuidText.setText(item.uuid); + + destroyButton.setOnClickListener(v -> { + if (listener != null) { + listener.onDestroy(item); + } + }); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/MainActivity.java b/android/app/src/main/java/seven/lab/wstun/ui/MainActivity.java new file mode 100644 index 0000000..ce5bd40 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/MainActivity.java @@ -0,0 +1,142 @@ +package seven.lab.wstun.ui; + +import android.Manifest; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import seven.lab.wstun.R; +import seven.lab.wstun.service.WSTunService; + +/** + * Main activity with tabs for server status and configuration. + * The Status tab contains 3 sub-tabs: Running Instances, Installed Services, Marketplace. + */ +public class MainActivity extends AppCompatActivity { + + private static final int PERMISSION_REQUEST_CODE = 100; + + private WSTunService service; + private boolean bound = false; + + private TabLayout tabLayout; + private ViewPager2 viewPager; + + private StatusFragment statusFragment; + private ConfigFragment configFragment; + + private final ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + WSTunService.LocalBinder localBinder = (WSTunService.LocalBinder) binder; + service = localBinder.getService(); + bound = true; + + // Notify fragments + if (statusFragment != null) { + statusFragment.onServiceConnected(service); + } + if (configFragment != null) { + configFragment.onServiceConnected(service); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + bound = false; + service = null; + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + requestPermissions(); + initViews(); + bindService(); + } + + @Override + protected void onDestroy() { + if (bound) { + unbindService(connection); + bound = false; + } + super.onDestroy(); + } + + private void requestPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.POST_NOTIFICATIONS}, + PERMISSION_REQUEST_CODE); + } + } + } + + private void initViews() { + tabLayout = findViewById(R.id.tabLayout); + viewPager = findViewById(R.id.viewPager); + + statusFragment = new StatusFragment(); + configFragment = new ConfigFragment(); + + viewPager.setAdapter(new FragmentStateAdapter(this) { + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) { + return statusFragment; + } else { + return configFragment; + } + } + + @Override + public int getItemCount() { + return 2; + } + }); + + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + if (position == 0) { + tab.setText(R.string.tab_status); + } else { + tab.setText(R.string.tab_config); + } + }).attach(); + } + + private void bindService() { + Intent intent = new Intent(this, WSTunService.class); + bindService(intent, connection, Context.BIND_AUTO_CREATE); + } + + public WSTunService getWSTunService() { + return service; + } + + public boolean isServiceBound() { + return bound; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceAdapter.java b/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceAdapter.java new file mode 100644 index 0000000..90222b7 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceAdapter.java @@ -0,0 +1,114 @@ +package seven.lab.wstun.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import seven.lab.wstun.R; + +/** + * Adapter for displaying marketplace services. + */ +public class MarketplaceAdapter extends RecyclerView.Adapter { + + private List services = new ArrayList<>(); + private final InstallListener listener; + + public interface InstallListener { + void onInstall(String serviceName); + } + + public static class MarketplaceItem { + public final String name; + public final String displayName; + public final String description; + public final String version; + public final boolean installed; + + public MarketplaceItem(String name, String displayName, String description, + String version, boolean installed) { + this.name = name; + this.displayName = displayName != null ? displayName : name; + this.description = description; + this.version = version; + this.installed = installed; + } + } + + public MarketplaceAdapter(InstallListener listener) { + this.listener = listener; + } + + public void setServices(List services) { + this.services = services; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_marketplace_service, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + MarketplaceItem item = services.get(position); + holder.bind(item, listener); + } + + @Override + public int getItemCount() { + return services.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView nameText; + private final TextView descText; + private final TextView versionText; + private final Button installButton; + + ViewHolder(View itemView) { + super(itemView); + nameText = itemView.findViewById(R.id.serviceNameText); + descText = itemView.findViewById(R.id.serviceDescText); + versionText = itemView.findViewById(R.id.versionText); + installButton = itemView.findViewById(R.id.installButton); + } + + void bind(MarketplaceItem item, InstallListener listener) { + nameText.setText(item.displayName); + + if (item.description != null && !item.description.isEmpty()) { + descText.setText(item.description); + descText.setVisibility(View.VISIBLE); + } else { + descText.setVisibility(View.GONE); + } + + versionText.setText("v" + item.version); + + if (item.installed) { + installButton.setText("Installed"); + installButton.setEnabled(false); + } else { + installButton.setText("Install"); + installButton.setEnabled(true); + installButton.setOnClickListener(v -> { + if (listener != null) { + listener.onInstall(item.name); + } + }); + } + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceFragment.java new file mode 100644 index 0000000..702475b --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceFragment.java @@ -0,0 +1,205 @@ +package seven.lab.wstun.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.textfield.TextInputEditText; +import com.google.gson.JsonObject; + +import java.util.ArrayList; +import java.util.List; + +import seven.lab.wstun.R; +import seven.lab.wstun.marketplace.MarketplaceService; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment for marketplace service browsing and installation. + */ +public class MarketplaceFragment extends Fragment { + + private WSTunService service; + + private TextInputEditText urlInput; + private Button fetchButton; + private ProgressBar progressBar; + private RecyclerView servicesRecyclerView; + private TextView emptyText; + private TextView errorText; + + private MarketplaceAdapter adapter; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_marketplace, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + urlInput = view.findViewById(R.id.marketplaceUrlInput); + fetchButton = view.findViewById(R.id.fetchButton); + progressBar = view.findViewById(R.id.progressBar); + servicesRecyclerView = view.findViewById(R.id.servicesRecyclerView); + emptyText = view.findViewById(R.id.emptyText); + errorText = view.findViewById(R.id.errorText); + + servicesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new MarketplaceAdapter(this::installService); + servicesRecyclerView.setAdapter(adapter); + + fetchButton.setOnClickListener(v -> fetchMarketplace()); + + // Load saved marketplace URL + if (service != null && service.getLocalServiceManager() != null) { + String savedUrl = service.getLocalServiceManager().getMarketplaceService().getMarketplaceUrl(); + if (savedUrl != null && !savedUrl.isEmpty()) { + urlInput.setText(savedUrl); + } + } + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + + if (urlInput != null && service.getLocalServiceManager() != null) { + String savedUrl = service.getLocalServiceManager().getMarketplaceService().getMarketplaceUrl(); + if (savedUrl != null && !savedUrl.isEmpty()) { + urlInput.setText(savedUrl); + } + } + } + + private void fetchMarketplace() { + if (service == null || service.getLocalServiceManager() == null) { + showError("Service not connected"); + return; + } + + String url = urlInput.getText() != null ? urlInput.getText().toString().trim() : ""; + if (url.isEmpty()) { + showError("Please enter a marketplace URL"); + return; + } + + hideError(); + showLoading(true); + + MarketplaceService marketplace = service.getLocalServiceManager().getMarketplaceService(); + marketplace.setMarketplaceUrl(url); + + marketplace.listMarketplace(url, new MarketplaceService.MarketplaceCallback>() { + @Override + public void onSuccess(List result) { + if (getActivity() == null) return; + getActivity().runOnUiThread(() -> { + showLoading(false); + displayServices(result); + }); + } + + @Override + public void onError(String error) { + if (getActivity() == null) return; + getActivity().runOnUiThread(() -> { + showLoading(false); + showError("Failed to fetch: " + error); + adapter.setServices(new ArrayList<>()); + }); + } + }); + } + + private void displayServices(List services) { + List items = new ArrayList<>(); + MarketplaceService marketplace = service.getLocalServiceManager().getMarketplaceService(); + + for (JsonObject svc : services) { + String name = svc.has("name") ? svc.get("name").getAsString() : null; + if (name == null) continue; + + String displayName = svc.has("displayName") ? svc.get("displayName").getAsString() : name; + String description = svc.has("description") ? svc.get("description").getAsString() : null; + String version = svc.has("version") ? svc.get("version").getAsString() : "1.0.0"; + boolean installed = marketplace.isInstalled(name); + + items.add(new MarketplaceAdapter.MarketplaceItem(name, displayName, description, version, installed)); + } + + adapter.setServices(items); + + if (items.isEmpty()) { + emptyText.setVisibility(View.VISIBLE); + servicesRecyclerView.setVisibility(View.GONE); + } else { + emptyText.setVisibility(View.GONE); + servicesRecyclerView.setVisibility(View.VISIBLE); + } + } + + private void installService(String serviceName) { + if (service == null || service.getLocalServiceManager() == null) { + showError("Service not connected"); + return; + } + + String url = urlInput.getText() != null ? urlInput.getText().toString().trim() : ""; + if (url.isEmpty()) { + showError("No marketplace URL"); + return; + } + + showLoading(true); + + MarketplaceService marketplace = service.getLocalServiceManager().getMarketplaceService(); + marketplace.installService(url, serviceName, new MarketplaceService.MarketplaceCallback() { + @Override + public void onSuccess(seven.lab.wstun.marketplace.InstalledService result) { + if (getActivity() == null) return; + getActivity().runOnUiThread(() -> { + showLoading(false); + Toast.makeText(getContext(), "Service installed successfully", Toast.LENGTH_SHORT).show(); + fetchMarketplace(); // Refresh list + }); + } + + @Override + public void onError(String error) { + if (getActivity() == null) return; + getActivity().runOnUiThread(() -> { + showLoading(false); + showError("Install failed: " + error); + }); + } + }); + } + + private void showLoading(boolean show) { + progressBar.setVisibility(show ? View.VISIBLE : View.GONE); + fetchButton.setEnabled(!show); + } + + private void showError(String message) { + errorText.setText(message); + errorText.setVisibility(View.VISIBLE); + } + + private void hideError() { + errorText.setVisibility(View.GONE); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/RunningInstancesFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/RunningInstancesFragment.java new file mode 100644 index 0000000..d6ad09b --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/RunningInstancesFragment.java @@ -0,0 +1,126 @@ +package seven.lab.wstun.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import seven.lab.wstun.R; +import seven.lab.wstun.marketplace.InstalledService; +import seven.lab.wstun.server.LocalServiceManager; +import seven.lab.wstun.server.ServiceManager; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment showing running service instances (rooms). + */ +public class RunningInstancesFragment extends Fragment { + + private WSTunService service; + + private RecyclerView instancesRecyclerView; + private TextView emptyText; + + private InstanceAdapter adapter; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_running_instances, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + instancesRecyclerView = view.findViewById(R.id.servicesRecyclerView); + emptyText = view.findViewById(R.id.emptyText); + + instancesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new InstanceAdapter(instance -> { + if (service != null && service.getServiceManager() != null) { + service.getServiceManager().destroyInstance(instance.getUuid()); + updateInstanceList(); + } + }); + instancesRecyclerView.setAdapter(adapter); + + updateInstanceList(); + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + updateInstanceList(); + } + + @Override + public void onResume() { + super.onResume(); + updateInstanceList(); + } + + public void updateInstanceList() { + if (getActivity() == null || !isAdded()) return; + + requireActivity().runOnUiThread(() -> { + if (service == null || service.getServiceManager() == null) { + adapter.setInstances(new ArrayList<>()); + emptyText.setVisibility(View.VISIBLE); + instancesRecyclerView.setVisibility(View.GONE); + return; + } + + LocalServiceManager lsm = service.getLocalServiceManager(); + ServiceManager sm = service.getServiceManager(); + + // Collect all instances across all services + List items = new ArrayList<>(); + + if (lsm != null) { + Map installed = lsm.getInstalledServices(); + for (String serviceName : installed.keySet()) { + List instances = sm.getInstancesForService(serviceName); + InstalledService svc = installed.get(serviceName); + String displayName = svc != null ? svc.getDisplayName() : serviceName; + + for (ServiceManager.ServiceInstance inst : instances) { + items.add(new InstanceAdapter.InstanceItem( + inst.getUuid(), + inst.getName(), + serviceName, + displayName, + inst.getUserCount() + )); + } + } + } + + adapter.setInstances(items); + + if (items.isEmpty()) { + emptyText.setVisibility(View.VISIBLE); + instancesRecyclerView.setVisibility(View.GONE); + } else { + emptyText.setVisibility(View.GONE); + instancesRecyclerView.setVisibility(View.VISIBLE); + } + }); + } + + // Renamed method to match StatusFragment expectations + public void updateServiceList() { + updateInstanceList(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/ServiceAdapter.java b/android/app/src/main/java/seven/lab/wstun/ui/ServiceAdapter.java new file mode 100644 index 0000000..3c94506 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/ServiceAdapter.java @@ -0,0 +1,89 @@ +package seven.lab.wstun.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import seven.lab.wstun.R; +import seven.lab.wstun.server.ServiceManager; + +/** + * RecyclerView adapter for displaying registered services. + */ +public class ServiceAdapter extends RecyclerView.Adapter { + + public interface KickCallback { + void onKick(ServiceManager.ServiceEntry service); + } + + private List services = new ArrayList<>(); + private final KickCallback callback; + private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); + + public ServiceAdapter(KickCallback callback) { + this.callback = callback; + } + + public void setServices(List services) { + this.services = new ArrayList<>(services); + notifyDataSetChanged(); + } + + @NonNull + @Override + public ServiceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_service, parent, false); + return new ServiceViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ServiceViewHolder holder, int position) { + ServiceManager.ServiceEntry service = services.get(position); + holder.bind(service); + } + + @Override + public int getItemCount() { + return services.size(); + } + + class ServiceViewHolder extends RecyclerView.ViewHolder { + private final TextView serviceName; + private final TextView serviceType; + private final TextView serviceEndpoint; + private final Button kickButton; + + ServiceViewHolder(View itemView) { + super(itemView); + serviceName = itemView.findViewById(R.id.serviceName); + serviceType = itemView.findViewById(R.id.serviceType); + serviceEndpoint = itemView.findViewById(R.id.serviceEndpoint); + kickButton = itemView.findViewById(R.id.kickButton); + } + + void bind(ServiceManager.ServiceEntry service) { + serviceName.setText(service.getName()); + serviceType.setText(service.getType() + " - Connected at " + + dateFormat.format(new Date(service.getRegisteredAt()))); + serviceEndpoint.setText("/" + service.getName() + "/main"); + + kickButton.setOnClickListener(v -> { + if (callback != null) { + callback.onKick(service); + } + }); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/StatusFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/StatusFragment.java new file mode 100644 index 0000000..467137e --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/StatusFragment.java @@ -0,0 +1,247 @@ +package seven.lab.wstun.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Collections; +import java.util.List; + +import seven.lab.wstun.R; +import seven.lab.wstun.server.ServiceManager; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment showing server status with nested tabs for: + * - Running Instances + * - Installed Services + * - Marketplace + */ +public class StatusFragment extends Fragment implements WSTunService.ServiceListener { + + private WSTunService service; + + private TextView statusText; + private TextView addressText; + private Button toggleButton; + + // Nested tabs + private TabLayout innerTabLayout; + private ViewPager2 innerViewPager; + + // Child fragments + private RunningInstancesFragment runningInstancesFragment; + private InstalledServicesFragment installedServicesFragment; + private MarketplaceFragment marketplaceFragment; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_status, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + statusText = view.findViewById(R.id.statusText); + addressText = view.findViewById(R.id.addressText); + toggleButton = view.findViewById(R.id.toggleButton); + innerTabLayout = view.findViewById(R.id.innerTabLayout); + innerViewPager = view.findViewById(R.id.innerViewPager); + + // Set up nested tabs + setupInnerTabs(); + + toggleButton.setOnClickListener(v -> toggleServer()); + + updateUI(); + } + + private void setupInnerTabs() { + runningInstancesFragment = new RunningInstancesFragment(); + installedServicesFragment = new InstalledServicesFragment(); + marketplaceFragment = new MarketplaceFragment(); + + innerViewPager.setAdapter(new FragmentStateAdapter(this) { + @NonNull + @Override + public Fragment createFragment(int position) { + switch (position) { + case 0: + return runningInstancesFragment; + case 1: + return installedServicesFragment; + case 2: + return marketplaceFragment; + default: + return runningInstancesFragment; + } + } + + @Override + public int getItemCount() { + return 3; + } + }); + + new TabLayoutMediator(innerTabLayout, innerViewPager, (tab, position) -> { + switch (position) { + case 0: + tab.setText(R.string.tab_instances); + break; + case 1: + tab.setText(R.string.tab_services); + break; + case 2: + tab.setText(R.string.tab_marketplace); + break; + } + }).attach(); + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + service.setListener(this); + updateUI(); + + // Pass service to child fragments + if (runningInstancesFragment != null) { + runningInstancesFragment.onServiceConnected(service); + } + if (installedServicesFragment != null) { + installedServicesFragment.onServiceConnected(service); + } + if (marketplaceFragment != null) { + marketplaceFragment.onServiceConnected(service); + } + } + + @Override + public void onResume() { + super.onResume(); + updateUI(); + // Refresh all child fragments when returning to the app + if (runningInstancesFragment != null) { + runningInstancesFragment.updateInstanceList(); + } + if (installedServicesFragment != null) { + installedServicesFragment.onResume(); + } + } + + private void toggleServer() { + if (service == null) return; + + if (service.isServerRunning()) { + Intent intent = new Intent(getContext(), WSTunService.class); + intent.setAction(WSTunService.ACTION_STOP); + requireContext().startService(intent); + } else { + Intent intent = new Intent(getContext(), WSTunService.class); + intent.setAction(WSTunService.ACTION_START); + requireContext().startForegroundService(intent); + } + } + + private void updateUI() { + if (getActivity() == null || !isAdded()) return; + + requireActivity().runOnUiThread(() -> { + boolean running = service != null && service.isServerRunning(); + + if (running) { + statusText.setText(R.string.server_running); + statusText.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); + toggleButton.setText(R.string.stop_server); + + String protocol = service.isHttpsEnabled() ? "https" : "http"; + String ip = getLocalIpAddress(); + String address = protocol + "://" + ip + ":" + service.getPort(); + addressText.setText(address); + addressText.setVisibility(View.VISIBLE); + } else { + statusText.setText(R.string.server_stopped); + statusText.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + toggleButton.setText(R.string.start_server); + addressText.setVisibility(View.GONE); + } + }); + } + + private String getLocalIpAddress() { + try { + List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + for (NetworkInterface intf : interfaces) { + List addrs = Collections.list(intf.getInetAddresses()); + for (InetAddress addr : addrs) { + if (!addr.isLoopbackAddress()) { + String sAddr = addr.getHostAddress(); + if (sAddr != null && !sAddr.contains(":")) { + return sAddr; + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return "localhost"; + } + + @Override + public void onServerStarted() { + updateUI(); + // Pass service to child fragments now that server is ready + if (runningInstancesFragment != null) { + runningInstancesFragment.onServiceConnected(service); + } + if (installedServicesFragment != null) { + installedServicesFragment.onServiceConnected(service); + } + if (marketplaceFragment != null) { + marketplaceFragment.onServiceConnected(service); + } + } + + @Override + public void onServerStopped() { + updateUI(); + if (runningInstancesFragment != null) { + runningInstancesFragment.updateServiceList(); + } + } + + @Override + public void onServiceChanged() { + if (getActivity() != null && runningInstancesFragment != null) { + requireActivity().runOnUiThread(() -> runningInstancesFragment.updateServiceList()); + } + } + + @Override + public void onError(String message) { + if (getActivity() != null) { + requireActivity().runOnUiThread(() -> + Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show() + ); + } + } +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100755 index 1cb7aa2..0000000 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/drawable/badge_background.xml b/android/app/src/main/res/drawable/badge_background.xml new file mode 100644 index 0000000..0b7dcf7 --- /dev/null +++ b/android/app/src/main/res/drawable/badge_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100755 index 8403758..0000000 --- a/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ee0e042 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_config.xml b/android/app/src/main/res/layout/fragment_config.xml new file mode 100644 index 0000000..71ea366 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_config.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +``` + +### User Client (main.html) + +```html + + + + FileShare + + +

FileShare

+
Connecting...
+ +
+ + + + + +``` + +## Message Types + +### Standard Messages + +| Type | Direction | Description | +|------|-----------|-------------| +| `create_instance` | Host → Server | Create instance | +| `instance_created` | Server → Host | Instance created | +| `join_instance` | Client → Server | Join instance | +| `list_instances` | Client → Server | List instances | +| `instance_list` | Server → Client | Instance list | +| `kick_client` | Host → Server | Kick user | +| `kick` | Server → Client | You were kicked | +| `ack` | Server → Client | Operation acknowledged | +| `error` | Server → Client | Error occurred | +| `client_connected` | Server → Host | User joined | +| `client_disconnected` | Server → Host | User left | + +### Custom Messages + +Services can define their own message types. Just use `send()` or `broadcast()`: + +```javascript +// From user client +client.send('my_custom_type', { data: 'value' }); + +// From host to all users +host.broadcast('my_custom_type', { data: 'value' }); +``` + +## Error Handling + +Both host and client support error callbacks: + +```javascript +const client = WSTun.createInstanceClient({ + // ... + onError: (err) => { + console.error('Connection error:', err.message); + // Possible errors: + // - Invalid or missing server auth token + // - Invalid instance token + // - Instance not found + // - Connection failed + } +}); +``` + +## Tips + +1. **Generate unique user IDs**: Use `WSTun.generateId()` or your own UUID generator +2. **Handle disconnections**: Users may disconnect at any time +3. **Validate data**: Don't trust data from other clients +4. **Use instance tokens**: For private rooms, always set an instance token +5. **Broadcast state changes**: Keep all users in sync by broadcasting updates diff --git a/android/docs/marketplace.md b/android/docs/marketplace.md new file mode 100644 index 0000000..6143b92 --- /dev/null +++ b/android/docs/marketplace.md @@ -0,0 +1,377 @@ +# WSTun Marketplace + +## Overview + +The WSTun Marketplace allows users to discover, install, and manage services from external sources. This document explains how to create a marketplace server and publish services. + +## Marketplace API + +A marketplace is simply an HTTP server that provides a specific API. WSTun clients fetch service information and files from the marketplace URL. + +### API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `{baseUrl}/list` | GET | List all available services | +| `{baseUrl}/service/{name}.json` | GET | Get service manifest | +| `{baseUrl}/service/{name}/{file}` | GET | Download service file | + +### Example Marketplace Structure + +``` +https://example.com/wstun/marketplace/ +├── list # Service list JSON +└── service/ + ├── myservice.json # Service manifest + └── myservice/ + ├── index.html # Service controller + └── main.html # User client +``` + +## Creating a Marketplace + +### 1. Service List (`/list`) + +Returns a JSON array of available services: + +```json +[ + { + "name": "myservice", + "displayName": "My Awesome Service", + "description": "A great service for doing things", + "version": "1.0.0", + "author": "Your Name" + }, + { + "name": "anotherservice", + "displayName": "Another Service", + "description": "Does other things", + "version": "2.1.0", + "author": "Someone Else" + } +] +``` + +### 2. Service Manifest (`/service/{name}.json`) + +Each service needs a manifest file that describes its structure: + +```json +{ + "name": "myservice", + "displayName": "My Awesome Service", + "description": "A great service for doing things", + "version": "1.0.0", + "author": "Your Name", + "icon": "https://example.com/icon.png", + "endpoints": [ + { + "path": "/service", + "file": "index.html", + "type": "service" + }, + { + "path": "/main", + "file": "main.html", + "type": "client" + } + ] +} +``` + +#### Manifest Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Unique service identifier (alphanumeric, lowercase) | +| `displayName` | No | Human-readable name | +| `description` | No | Short description of the service | +| `version` | No | Semantic version string | +| `author` | No | Author name or organization | +| `icon` | No | URL to service icon | +| `endpoints` | Yes | Array of endpoint definitions | + +#### Endpoint Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `path` | Yes | URL path (e.g., `/service`, `/main`) | +| `file` | Yes | Filename to serve for this path | +| `type` | Yes | `service` (controller) or `client` (user) | +| `contentType` | No | MIME type (auto-detected from extension if not set) | + +### 3. Service Files (`/service/{name}/{file}`) + +The actual HTML/JS files for your service. These should use `libwstun.js` for communication. + +## Example: Creating a Simple Service + +### 1. Create the Service Controller (`index.html`) + +```html + + + + + My Service - Controller + + +

My Service

+ +
+
+ + + + + +``` + +### 2. Create the User Client (`main.html`) + +```html + + + + + My Service + + +

My Service

+
Loading...
+ + +
+ + + + + +``` + +### 3. Create the Manifest (`myservice.json`) + +```json +{ + "name": "myservice", + "displayName": "My Custom Service", + "description": "A simple example service", + "version": "1.0.0", + "author": "Your Name", + "endpoints": [ + { + "path": "/service", + "file": "index.html", + "type": "service" + }, + { + "path": "/main", + "file": "main.html", + "type": "client" + } + ] +} +``` + +### 4. Update the Service List (`list`) + +```json +[ + { + "name": "myservice", + "displayName": "My Custom Service", + "description": "A simple example service", + "version": "1.0.0", + "author": "Your Name" + } +] +``` + +## Hosting a Marketplace + +### Option 1: Static File Server + +Host files on any static file server (Apache, Nginx, S3, GitHub Pages): + +``` +/marketplace/ + list # Service list JSON + service/ + myservice.json # Manifest + myservice/ + index.html # Controller + main.html # Client +``` + +Make sure CORS headers allow access from the WSTun server. + +### Option 2: Dynamic Server + +Create a dynamic server (Node.js, Python, etc.) that generates the list and serves files: + +```javascript +// Express.js example +const express = require('express'); +const app = express(); + +// Enable CORS +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + next(); +}); + +// Service list +app.get('/marketplace/list', (req, res) => { + res.json([ + { name: 'myservice', displayName: 'My Service', ... } + ]); +}); + +// Service manifest +app.get('/marketplace/service/:name.json', (req, res) => { + res.sendFile(`./services/${req.params.name}/manifest.json`); +}); + +// Service files +app.get('/marketplace/service/:name/:file', (req, res) => { + res.sendFile(`./services/${req.params.name}/${req.params.file}`); +}); + +app.listen(9090); +``` + +## Installing Services from Marketplace + +### Via Admin UI + +1. Open the WSTun server in a browser +2. Click "Service Manager" (or go to `/admin`) +3. Go to the "Marketplace" tab +4. Enter the marketplace URL +5. Click "Fetch" to see available services +6. Click "Install" on the desired service + +### Via API + +```javascript +// POST /_api/marketplace/install +fetch('/_api/marketplace/install', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: 'https://example.com/wstun/marketplace', + name: 'myservice' + }) +}); +``` + +## Best Practices + +1. **Use semantic versioning**: Makes it easy to track updates +2. **Write clear descriptions**: Help users understand what your service does +3. **Test thoroughly**: Ensure your service works with the WSTun server +4. **Include documentation**: Add comments or a README in your service +5. **Handle errors gracefully**: Show user-friendly error messages +6. **Use HTTPS**: Secure your marketplace server +7. **Add CORS headers**: Allow cross-origin requests from WSTun servers + +## Security Considerations + +1. **Validate service names**: Only alphanumeric and lowercase +2. **Sanitize file paths**: Prevent directory traversal attacks +3. **Review code**: Marketplace services can run arbitrary JavaScript +4. **Use HTTPS**: Encrypt data in transit +5. **Implement rate limiting**: Prevent abuse of your marketplace + +## Troubleshooting + +### "Failed to fetch marketplace" +- Check the marketplace URL is correct +- Ensure CORS headers are set +- Verify the `/list` endpoint returns valid JSON + +### "Failed to install service" +- Check the manifest is valid JSON +- Verify all endpoint files exist +- Check file permissions on the server + +### Service doesn't appear after install +- Refresh the service list +- Check the browser console for errors +- Verify the service files were downloaded correctly diff --git a/android/gradle.properties b/android/gradle.properties old mode 100755 new mode 100644 index 599d206..2e11322 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,6 +1,3 @@ -org.gradle.jvmargs=-Xmx4G -android.useAndroidX=true -android.enableJetifier=true -android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false -android.nonFinalResIds=false +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties old mode 100755 new mode 100644 index 81fb745..62f495d --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..31296a5 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,207 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var)}», «## », «## */», «#%»; +# * compoundeli commands having a redirection after the closing «)»; +# * «else» with no «if» line at the same nesting level; +# * arrays. +# +# (2) This script passes all arguments to the gradle program as a single +# argument, but preserves internal quoting, so you can say +# +# gradlew "foo bar" baz +# +# and Gradle will receive two arguments: "foo bar" and "baz". +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't touch options #( + /?*) t=${arg#)}; t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # temporary args, so each arg winds up back in the position where + # it started, but possibly modified. + # + # NB: aass assignment://[[]= is used to discard the value + # temporarily args, so each arg winds up back in the position where + # it started, but possibly modified. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, GRADLE_OPTS, and GRADLE_USER_HOME are used to compute +# the final set of JVM options. +# * GRADLE_OPTS and GRADLE_USER_HOME are examined for their content because Gradle +# puts commonly-used JVM arguments there. +# * The user's JVM arguments or defaults are added to the java command's JVM arguments. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xeli" is not enabled or the shell is not bash. +if ! "$cygwin" && ! "$msys" ; then + exec "$JAVACMD" "$@" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle old mode 100755 new mode 100644 index ee56b97..97edfd1 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,29 +1,17 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.2.1' apply false -} - -include ":app" +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "NodeBase" +include ':app' diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100755 index ad322bc..0000000 --- a/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.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/ephemeral/ -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 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100755 index e041d38..0000000 --- a/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 - - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig deleted file mode 100755 index 0b2d479..0000000 --- a/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig deleted file mode 100755 index 0b2d479..0000000 --- a/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100755 index b63de6e..0000000 --- a/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,609 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 331C80F3294D02FB00263BE5 /* RunnerTests.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 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 */ - 331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 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 = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80F3294D02FB00263BE5 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80EE294D02FB00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80F2294D02FB00263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80F3294D02FB00263BE5 /* RunnerTests.m */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 331C80F2294D02FB00263BE5 /* RunnerTests */, - 97C146EF1CF9000F007C117D /* Products */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80F0294D02FB00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80ED294D02FB00263BE5 /* Sources */, - 331C80EE294D02FB00263BE5 /* Frameworks */, - 331C80EF294D02FB00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80F6294D02FB00263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - 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 = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80F0294D02FB00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C80F0294D02FB00263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80EF294D02FB00263BE5 /* 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 */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - 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"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80ED294D02FB00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80F6294D02FB00263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - 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 = 12.0; - 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 = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C80F8294D02FB00263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C80F9294D02FB00263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C80FA294D02FB00263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - 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 = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - 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 = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80F8294D02FB00263BE5 /* Debug */, - 331C80F9294D02FB00263BE5 /* Release */, - 331C80FA294D02FB00263BE5 /* 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 */; -} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index c4b79bd..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index fc6bf80..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100755 index af0309c..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100755 index 2de4fc5..0000000 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 59c6d39..0000000 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index fc6bf80..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100755 index af0309c..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner/AppDelegate.h b/ios/Runner/AppDelegate.h deleted file mode 100755 index a7bb489..0000000 --- a/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m deleted file mode 100755 index 569c492..0000000 --- a/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#import "AppDelegate.h" -#import "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100755 index 1950fd8..0000000 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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" - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100755 index dc9ada4..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100755 index 7353c41..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100755 index 797d452..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100755 index 6ed2d93..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100755 index 4cd7b00..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100755 index fe73094..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100755 index 321773c..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100755 index 797d452..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100755 index 502f463..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100755 index 0ec3034..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100755 index 0ec3034..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100755 index e9f5fea..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100755 index 84ac32a..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100755 index 8953cba..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100755 index 0467bf1..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100755 index d08a4de..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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" - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100755 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100755 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100755 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100755 index 65a94b5..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100755 index 497371e..0000000 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100755 index bbb83ca..0000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100755 index 92b36e2..0000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nodebase - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - nodebase - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/ios/Runner/main.m b/ios/Runner/main.m deleted file mode 100755 index 4618607..0000000 --- a/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/ios/RunnerTests/RunnerTests.m b/ios/RunnerTests/RunnerTests.m deleted file mode 100755 index 478eaec..0000000 --- a/ios/RunnerTests/RunnerTests.m +++ /dev/null @@ -1,16 +0,0 @@ -#import -#import -#import - -@interface RunnerTests : XCTestCase - -@end - -@implementation RunnerTests - -- (void)testExample { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. -} - -@end diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100755 index 6e9b359..0000000 --- a/lib/main.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter/material.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); - } -} diff --git a/lib/util/api.dart b/lib/util/api.dart deleted file mode 100755 index 01e6be4..0000000 --- a/lib/util/api.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'dart:developer'; - -class NodeBaseApi { - static const api = MethodChannel('net.seven.nodebase/app'); - static const event = EventChannel('net.seven.nodebase/event'); - - static Future> apiUtilGetIPs() async { - /* { - : ["", "", ...] - } */ - try { - var json = await api.invokeMethod('util.ip'); - Map r = {}; - json.forEach((key, value) { - r[key.toString()] = value; - }); - return Future.value(r); - } catch (e) { - log("NodeBase [E] getIPs / ${e.toString()}"); - return {}; - } - } - - static Future apiUtilMarkExecutable(String filename) async { - try { - await api.invokeMethod( - 'util.file.executable', - {"filename": filename} - ); - } catch (e) { - log("NodeBase [E] utilMarkExecutable / ${e.toString()}"); - } - } - - static Future> apiAppStatus(String app) async { - /* { - state: "none" | "new" | "running" | "dead" - } */ - try { - var json = await api.invokeMethod('app.stat', {"name": app}); - Map r = {}; - json.forEach((key, value) { - r[key.toString()] = value; - }); - return r; - } catch (e) { - log("NodeBase [E] appStatus / ${e.toString()}"); - return {}; - } - } - - static Future apiAppStart(String app, List cmd) async { - try { - await api.invokeMethod( - 'app.start', {"name": app, "cmd": cmd.join("\x01")} - ); - } catch (e) { - log("NodeBase [E] appStart / ${e.toString()}"); - } - } - - static Future apiAppStop(String app) async { - try { - await api.invokeMethod( - 'app.stop', {"name": app} - ); - } catch (e) { - log("NodeBase [E] appStop / ${e.toString()}"); - } - } - - static Future apiAppOpenBrowser(String url) async { - try { - api.invokeMethod('util.browser.open', { - "url": url, - }); - } catch (e) { - log("NodeBase [E] appOpenBrowser / ${e.toString()}"); - } - } -} \ No newline at end of file diff --git a/lib/util/fs.dart b/lib/util/fs.dart deleted file mode 100755 index 345a2e7..0000000 --- a/lib/util/fs.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:path_provider/path_provider.dart'; -import 'package:archive/archive.dart'; -import 'package:path/path.dart' as path; -import 'dart:io'; -import 'dart:developer'; - -// getApplicationDocumentsDirectory -> /data/data/app/... -// getExternalStorageDirectory -> /storage/sdcard-external/android/data/app/... - -Future get _appPath async { - final directory = await getApplicationDocumentsDirectory(); - return directory.path; -} - -Future fsGetAppFileReference(filepath) async { - final base = await _appPath; - return File(path.join(base, filepath)); -} - -Future fsReadAppFileAsString(filepath) async { - try { - final file = await fsGetAppFileReference(filepath); - String contents = await file.readAsString(); - return contents; - } catch (e) { - log("NodeBase [E] fsReadAppFileAsString / ${e.toString()}"); - return ""; - } -} - -Future fsWriteAppFileAsString(filepath, contents) async { - final file = await fsGetAppFileReference(filepath); - file.writeAsString(contents); -} - -Future fsGetEntity(filepath) async { - final base = await _appPath; - final filename = path.join(base, filepath); - final T = await FileSystemEntity.type(filename); - if (T == FileSystemEntityType.notFound) { - return "notFound"; - } - if (T == FileSystemEntityType.link) { - return Link(filepath); - } - if (T == FileSystemEntityType.file) { - return File(filepath); - } - return Directory(filepath); -} - -Future fsMkdir(filepath) async { - final path = await _appPath; - return await Directory("$path$filepath").create(recursive: true); -} - -Future> fsLs(filepath) async { - final base = await _appPath; - final filename = path.join(base, filepath); - final list = []; - final T = await FileSystemEntity.type(filename); - if (T == FileSystemEntityType.notFound) { - } else if (T == FileSystemEntityType.link) { - // ignore files under link directory - list.add(Link(filename)); - } else if (T == FileSystemEntityType.file) { - list.add(File(filename)); - } else { - final dir = Directory(filename); - final entities = await dir.list(recursive: false, followLinks: false).toList(); - for (var entity in entities) { - list.add(entity); - } - } - return list; -} - -Future fsGetAppBaseDir(String app) async { - // TODO: check ".." in app - final base = await _appPath; - final appBaseDir = path.join(base, "apps", app); - return appBaseDir; -} - -Future fsRemoveApp(String app) async { - // TODO: check ".." in app - if (app == "") return false; - final base = await _appPath; - final appBaseDir = path.join(base, "apps", app); - final dir = Directory(appBaseDir); - if (await dir.exists()) { - await dir.delete(recursive: true); - } - return true; -} - -Future fsMoveApp(String app, String newname) async { - // TODO: check ".." in app and newname - if (app == "" || newname == "" || app == newname) return false; - final base = await _appPath; - final appBaseDir = path.join(base, "apps", app); - final newBaseDir = path.join(base, "app", newname); - final dir = Directory(appBaseDir); - if (await dir.exists()) { - await dir.rename(newBaseDir); - } - return true; -} - -Future fsZipFiles(String zipFilename, List files) async { - // TODO: try...catch... - - // Create an empty archive - final archive = Archive(); - - for (final file in files) { - // Read the file bytes - final bytes = file.readAsBytesSync(); - - // Add the file to the archive - archive.addFile(ArchiveFile( - path.basename(file.path), // File name - bytes.length, // File size - bytes, // File data - )); - } - - // Encode the archive to Zip - final zipData = ZipEncoder().encode(archive); - - // Write the zipped bytes to a file - File(zipFilename) - ..createSync(recursive: true) // Create the file if it doesn't exist - ..writeAsBytesSync(zipData!); -} - -Future fsUnzipFiles(String zipFilename, String dstDir) async { - // TODO: try...catch... - final bytes = File(zipFilename).readAsBytesSync(); - - // Decode the Zip archive - final archive = ZipDecoder().decodeBytes(bytes); - - for (final file in archive) { - final filename = file.name; - if (file.isFile) { - final data = file.content as List; - File(path.join(dstDir, filename)) - ..createSync(recursive: true) - ..writeAsBytesSync(data); - } else { - Directory(path.join(dstDir, filename)) - .createSync(recursive: true); - } - } -} - -Future fsDownload(String filename, String url) async { - var urlobj = Uri.parse(url); - var client = HttpClient(); - try { - final req = await client.getUrl(urlobj); - final res = await req.close(); - res.pipe(File(filename).openWrite()); - } finally { - client.close(); - } -} - diff --git a/linux/.gitignore b/linux/.gitignore deleted file mode 100755 index c7ea17f..0000000 --- a/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt deleted file mode 100755 index 40114da..0000000 --- a/linux/CMakeLists.txt +++ /dev/null @@ -1,145 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "nodebase") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "net.seven.nodebase.nodebase") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Define the application target. To change its name, change BINARY_NAME above, -# not the value here, or `flutter run` will no longer work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt deleted file mode 100755 index 27860e8..0000000 --- a/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc deleted file mode 100755 index e71a16d..0000000 --- a/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h deleted file mode 100755 index e0f0a47..0000000 --- a/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake deleted file mode 100755 index 2e1de87..0000000 --- a/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc deleted file mode 100755 index 4340ffc..0000000 --- a/linux/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/linux/my_application.cc b/linux/my_application.cc deleted file mode 100755 index d83a9d1..0000000 --- a/linux/my_application.cc +++ /dev/null @@ -1,104 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "nodebase"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "nodebase"); - } - - gtk_window_set_default_size(window, 360, 720); - gtk_widget_show(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, - nullptr)); -} diff --git a/linux/my_application.h b/linux/my_application.h deleted file mode 100755 index 8f20fb5..0000000 --- a/linux/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore deleted file mode 100755 index d4e0569..0000000 --- a/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100755 index f022c34..0000000 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100755 index f022c34..0000000 --- a/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100755 index e777c67..0000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import path_provider_foundation - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) -} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100755 index 6118882..0000000 --- a/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,695 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* nodebase.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "nodebase.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* nodebase.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* nodebase.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nodebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nodebase"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nodebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nodebase"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nodebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nodebase"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - 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_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - 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_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - 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_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index fc6bf80..0000000 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100755 index 3dd3ce4..0000000 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 59c6d39..0000000 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index fc6bf80..0000000 --- a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift deleted file mode 100755 index 553a135..0000000 --- a/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100755 index 8d4e7cb..0000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100755 index 82b6f9d..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100755 index 13b35eb..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100755 index 0a3f5fa..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100755 index bdb5722..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100755 index f083318..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100755 index 326c0e7..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100755 index 2f1632c..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100755 index 797e9e0..0000000 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100755 index e3e5ade..0000000 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = nodebase - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2024 net.seven.nodebase. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig deleted file mode 100755 index b398823..0000000 --- a/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig deleted file mode 100755 index d93e5dc..0000000 --- a/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100755 index fb4d7d3..0000000 --- a/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements deleted file mode 100755 index 51d0967..0000000 --- a/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist deleted file mode 100755 index 3733c1a..0000000 --- a/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift deleted file mode 100755 index ab30cba..0000000 --- a/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements deleted file mode 100755 index 04336df..0000000 --- a/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift deleted file mode 100755 index ba12981..0000000 --- a/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import FlutterMacOS -import Cocoa -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100755 index 25f29d9..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,338 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: "direct main" - description: - name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.4.10" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.1" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.0" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.1" - collection: - dependency: transitive - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.18.0" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.3" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.6" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.1" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.18.1" - js: - dependency: transitive - description: - name: js - sha256: "4186c61b32f99e60f011f7160e32c89a758ae9b1d0c6d28e2c02ef0382300e2b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.7.0" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.12.16" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.5.0" - meta: - dependency: transitive - description: - name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.10.0" - path: - dependency: "direct main" - description: - name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.8.3" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.2" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.2" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.1" - platform: - dependency: transitive - description: - name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.4" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.7.4" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.6.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.4" - web: - dependency: transitive - description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.3.0" - win32: - dependency: transitive - description: - name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.2.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.4" -sdks: - dart: ">=3.2.4 <4.0.0" - flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100755 index e8c6cf8..0000000 --- a/pubspec.yaml +++ /dev/null @@ -1,96 +0,0 @@ -name: nodebase -description: "Running Node.js application over Wifi and share with your friends." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 - -environment: - sdk: '>=3.2.4 <4.0.0' - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - path: ^1.8.3 - path_provider: ^2.1.2 - archive: ^3.4.10 - flutter_localizations: - sdk: flutter - intl: any - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^3.0.1 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100755 index d6c7473..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:nodebase/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100755 index 8aaa46a..0000000 Binary files a/web/favicon.png and /dev/null differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png deleted file mode 100755 index b749bfe..0000000 Binary files a/web/icons/Icon-192.png and /dev/null differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png deleted file mode 100755 index 88cfd48..0000000 Binary files a/web/icons/Icon-512.png and /dev/null differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png deleted file mode 100755 index eb9b4d7..0000000 Binary files a/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png deleted file mode 100755 index d69c566..0000000 Binary files a/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/index.html b/web/index.html deleted file mode 100755 index 83cde70..0000000 --- a/web/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - nodebase - - - - - - - - - - diff --git a/web/manifest.json b/web/manifest.json deleted file mode 100755 index 8f6a3dc..0000000 --- a/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "nodebase", - "short_name": "nodebase", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "Running Node.js application over Wifi and share with your friends.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/windows/.gitignore b/windows/.gitignore deleted file mode 100755 index ec4098a..0000000 --- a/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt deleted file mode 100755 index 99b14a3..0000000 --- a/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(nodebase LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "nodebase") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(VERSION 3.14...3.25) - -# Define build configuration option. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() -# Define settings for the Profile build mode. -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt deleted file mode 100755 index efb62eb..0000000 --- a/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# Set fallback configurations for older versions of the flutter tool. -if (NOT DEFINED FLUTTER_TARGET_PLATFORM) - set(FLUTTER_TARGET_PLATFORM "windows-x64") -endif() - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - ${FLUTTER_TARGET_PLATFORM} $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc deleted file mode 100755 index 8b6d468..0000000 --- a/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h deleted file mode 100755 index dc139d8..0000000 --- a/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake deleted file mode 100755 index b93c4c3..0000000 --- a/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt deleted file mode 100755 index 2041a04..0000000 --- a/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc deleted file mode 100755 index 0f2758d..0000000 --- a/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "net.seven.nodebase" "\0" - VALUE "FileDescription", "nodebase" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "nodebase" "\0" - VALUE "LegalCopyright", "Copyright (C) 2024 net.seven.nodebase. All rights reserved." "\0" - VALUE "OriginalFilename", "nodebase.exe" "\0" - VALUE "ProductName", "nodebase" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_dart.h b/windows/runner/flutter_dart.h deleted file mode 100755 index fedc12d..0000000 --- a/windows/runner/flutter_dart.h +++ /dev/null @@ -1,406 +0,0 @@ -#ifndef _FLUTTER_DART_ -#define _FLUTTER_DART_ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include "utils.h" - -class NodeAppMonitor; -class NodeBaseEventChannelHandler; - -enum NodeAppSTAT { - BORN, READY, RUNNING, DEAD -}; - -class NodeAppMonitor { -public: - NodeAppMonitor(const std::string &name, const std::string &cmd) { - this->name = name; - this->cmd = cmd; - this->stat = NodeAppSTAT::BORN; - ZeroMemory(&this->pi, sizeof(this->pi)); - this->pth = std::thread(NodeAppMonitor::Run, this); - } - ~NodeAppMonitor() { - this->Stop(); - if (this->pth.joinable()) this->pth.join(); - } - - void Start() { - this->stat = NodeAppSTAT::READY; - STARTUPINFO si; - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&this->pi, sizeof(this->pi)); - std::vector cmdL; - this->SplitCmd(cmdL, this->cmd); - std::wstring prog = Utf8ToUtf16(cmdL.at(0).c_str()); - cmdL.erase(cmdL.begin()); - std::wstring cmdLine = Utf8ToUtf16( - std::accumulate( - cmdL.begin(), cmdL.end(), std::string(" ") - ).c_str() - ); - // ref: https://forums.codeguru.com/showthread.php?514716-std-string-to-LPSTR - LPWSTR cmdLineAdapter = &cmdLine.front(); - if (!CreateProcess( - prog.c_str(), - cmdLineAdapter, - nullptr, - nullptr, - FALSE, - 0, - nullptr, - nullptr, - &si, - &pi) - ) { - this->stat = NodeAppSTAT::DEAD; - printf( - "NodeAppMonitor [E] \"%s\" start failure on CreateProcess.", - this->name.c_str() - ); - return; - } - this->stat = NodeAppSTAT::RUNNING; - WaitForSingleObject( this->pi.hProcess, INFINITE ); - this->stat = NodeAppSTAT::DEAD; - CloseHandle( this->pi.hProcess ); - CloseHandle( this->pi.hThread ); - } - - void Stop() { - if (!this->IsRunning()) return; - if (!this->pi.hProcess) return; - TerminateProcess(this->pi.hProcess, 0); - this->stat = NodeAppSTAT::DEAD; - const DWORD r = WaitForSingleObject(this->pi.hProcess, 500); - if (r == WAIT_OBJECT_0) { - // TODO: ok - } else { - printf( - "NodeAppMonitor [E] \"%s\" stop failure on TerminateProcess.", - this->name.c_str() - ); - // TODO: failured - } - CloseHandle(this->pi.hProcess); - CloseHandle(this->pi.hThread); - } - - NodeAppMonitor* restart() { - this->Stop(); - return new NodeAppMonitor(this->name, this->cmd); - } - flutter::EncodableValue toJSON() { - std::string rstat = "none"; - switch (this->stat) { - case NodeAppSTAT::RUNNING: - rstat = "running"; - break; - case NodeAppSTAT::DEAD: - rstat = "dead"; - break; - case NodeAppSTAT::READY: - rstat = "new"; - break; - case NodeAppSTAT::BORN: - default: - break; - } - flutter::EncodableMap r = { - {flutter::EncodableValue("state"), flutter::EncodableValue(rstat)}, - }; - return flutter::EncodableValue(r); - } - - bool IsRunning() { return this->stat == NodeAppSTAT::RUNNING; } - bool IsDead() { return this->stat == NodeAppSTAT::DEAD; } - - std::string GetName() { return this->name; } - std::string GetCmd() { return this->cmd; } -private: - int SplitCmd(std::vector &vec, const std::string &cmdx0) { - int count = 0; - std::stringstream spliter(cmdx0); - std::string item; - vec.clear(); - while (std::getline(spliter, item, '\x01')) { - vec.push_back(item); - count++; - } - return count; - } - - static void Run(NodeAppMonitor* app) { - app->Start(); - } - -private: - NodeAppSTAT stat; - std::string name; - std::string cmd; - std::thread pth; - PROCESS_INFORMATION pi; -}; - -// ref: https://stackoverflow.com/questions/24764477/network-adapter-information-in-c -// ref: https://learn.microsoft.com/zh-cn/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses?redirectedfrom=MSDN -static int getIPAdresses(flutter::EncodableMap& out) { - int count = 0; - - char buf[32*1024]; - ULONG pAddresses_len = sizeof(buf); - IP_ADAPTER_ADDRESSES * pAddresses = (IP_ADAPTER_ADDRESSES *)buf; - DWORD dwRetVal = GetAdaptersAddresses( - AF_UNSPEC, - GAA_FLAG_INCLUDE_PREFIX, - nullptr, - pAddresses, - &pAddresses_len - ); - if (dwRetVal == NO_ERROR) { - IP_ADAPTER_ADDRESSES *pCurrAddresses = pAddresses; - out.clear(); - while (pCurrAddresses) { - int n = (int)(pCurrAddresses->PhysicalAddressLength); - if (n > 0) { - // pCurrAddresses->AdapterName is in a format like {xxxx-yyyy-zzzz...} - flutter::EncodableValue name(Utf8FromUtf16(pCurrAddresses->FriendlyName)); - flutter::EncodableList arr; - for (IP_ADAPTER_UNICAST_ADDRESS* pUnicast = pCurrAddresses->FirstUnicastAddress; pUnicast != NULL; pUnicast = pUnicast->Next) { - wchar_t ipAddress[INET6_ADDRSTRLEN] = {0}; - void* pAddr = nullptr; - - // Determine the family of the IP address - if (pUnicast->Address.lpSockaddr->sa_family == AF_INET) { - // It's an IPv4 address - pAddr = &((struct sockaddr_in*)pUnicast->Address.lpSockaddr)->sin_addr; - } else if (pUnicast->Address.lpSockaddr->sa_family == AF_INET6) { - // It's an IPv6 address - pAddr = &((struct sockaddr_in6*)pUnicast->Address.lpSockaddr)->sin6_addr; - } - - // Convert the binary IP address to a string - if (pAddr) { - InetNtop(pUnicast->Address.lpSockaddr->sa_family, pAddr, ipAddress, sizeof(ipAddress)); - arr.push_back(flutter::EncodableValue(Utf8FromUtf16(ipAddress))); - } - } - out.insert_or_assign(name, arr); - } - pCurrAddresses = pCurrAddresses->Next; - count++; - } - } else { - // TODO: handle error - count = -1; - } - return count; -} - -class NodeBaseEventChannelHandler { -public: - NodeBaseEventChannelHandler(std::string&& channel_name, flutter::FlutterEngine* flutter_instance) { - auto event_channel = - std::make_unique>( - flutter_instance->messenger(), channel_name, - &flutter::StandardMethodCodec::GetInstance()); - - auto event_channel_handler = std::make_unique< - flutter::StreamHandlerFunctions>( - [this]( - const flutter::EncodableValue* arguments, - std::unique_ptr>&& events - ) -> std::unique_ptr> { - std::string name = EncodableValue2String(arguments); - this->sink.insert_or_assign(name, std::move(events)); - return nullptr; - }, - [this](const flutter::EncodableValue* arguments) - -> std::unique_ptr> { - // TODO: replace as find and erase - std::string name = EncodableValue2String(arguments); - this->sink.insert_or_assign(name, nullptr); - return nullptr; - }); - event_channel->SetStreamHandler(std::move(event_channel_handler)); - } - - void postMessage(std::string&& name, std::string&& message) { - auto target_ = this->sink.find(name); - if (target_ == this->sink.end()) return; - if (!target_->second) return; - //auto target = target_->second; - target_->second->Success(flutter::EncodableValue(message)); - } -private: - std::string EncodableValue2String(const flutter::EncodableValue* val) { - if (val->IsNull()) { - return std::string(""); - } - - if (std::holds_alternative(*val)) { - return std::get(*val); - } - - return std::string(""); - } -private: - std::map>> sink; -}; - -static std::map services; -static std::mutex service_lock; -static std::unique_ptr eventHandler; - -void appStart(const std::string &name, const std::string &cmd) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - NodeAppMonitor *app; - if (app_ != services.end()) { - app = app_->second; - services.erase(name); - delete app; - } - app = new NodeAppMonitor(name, cmd); - // TODO: if (!app) {} // memory allocate failure - services.insert_or_assign(name, app); -} -void appStop(const std::string &name) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - if (app_ == services.end()) return; - NodeAppMonitor *app = app_->second; - app->Stop(); -} -bool appRestart(const std::string &name) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - if (app_ == services.end()) return false; - NodeAppMonitor *app = app_->second; - app->Stop(); - NodeAppMonitor *newapp = app->restart(); - if (newapp == nullptr) { - return false; - } - delete app; - services.insert_or_assign(name, newapp); - return true; -} -flutter::EncodableValue appStat(const std::string &name) { - auto app_ = services.find(name); - if (app_ == services.end()) return flutter::EncodableValue(flutter::EncodableMap()); - NodeAppMonitor *app = app_->second; - return app->toJSON(); -} -flutter::EncodableValue utilGetIPs() { - flutter::EncodableMap ips; - getIPAdresses(ips); - return flutter::EncodableValue(ips); -} -void utilBrowserOpen(const std::string &url) { - if (url.rfind("http:", 0) != 0 && url.rfind("https:", 0) != 0) return; - ShellExecuteA(0, 0, url.c_str(), 0, 0 , SW_SHOW ); -} - -#define RETURN_BADARG_ERR(x) { result->Error("BAD_ARGS", "Invalid argument type for '" #x "'"); return; } -void InitMethodChannel(flutter::FlutterEngine* flutter_instance) { - // name your channel - const static std::string channel_name("net.seven.nodebase/app"); - - auto channel = - std::make_unique>( - flutter_instance->messenger(), channel_name, - &flutter::StandardMethodCodec::GetInstance()); - - channel->SetMethodCallHandler( - [](const flutter::MethodCall& call, - std::unique_ptr> result) { - - const auto* args = std::get_if(call.arguments()); - if (call.method_name().compare("app.stat") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(app.stat); - auto name_ = args->find(flutter::EncodableValue("name")); - if (name_ == args->end()) RETURN_BADARG_ERR(app.stat); - std::string name = std::get(name_->second); - result->Success(appStat(name)); - } - else if (call.method_name().compare("app.start") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(app.start); - auto name_ = args->find(flutter::EncodableValue("name")); - if (name_ == args->end()) RETURN_BADARG_ERR(app.start); - auto cmd_ = args->find(flutter::EncodableValue("cmd")); - if (cmd_ == args->end()) RETURN_BADARG_ERR(app.start); - std::string name = std::get(name_->second); - std::string cmd = std::get(cmd_->second); - appStart(name, cmd); - result->Success(); - } - else if (call.method_name().compare("app.restart") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(app.restart); - auto name_ = args->find(flutter::EncodableValue("name")); - if (name_ == args->end()) RETURN_BADARG_ERR(app.restart); - std::string name = std::get(name_->second); - if (!appRestart(name)) { - std::ostringstream msg; - msg << "Restart failed for app \"" << name << "\""; - result->Error("FAILURE", msg.str()); - return; - } - result->Success(); - } - else if (call.method_name().compare("app.stop") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(app.stop); - auto name_ = args->find(flutter::EncodableValue("name")); - if (name_ == args->end()) RETURN_BADARG_ERR(app.stop); - std::string name = std::get(name_->second); - appStop(name); - result->Success(); - } - else if (call.method_name().compare("util.ip") == 0) { - result->Success(utilGetIPs()); - } - else if (call.method_name().compare("util.file.executable") == 0) { - // in windows, we may not need to implement this; - // PE file can be executable directly without additional flag by default - /* - if (args == nullptr) RETURN_BADARG_ERR(util.file.executable); - auto name = args->find(flutter::EncodableValue("filename"))->second; - if (name.IsNull()) RETURN_BADARG_ERR(util.file.executable); - ... - */ - result->Success(); - } - else if (call.method_name().compare("util.browser.open") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(util.browser.open); - auto url_ = args->find(flutter::EncodableValue("url")); - if (url_ == args->end()) RETURN_BADARG_ERR(util.browser.open); - std::string url = std::get(url_->second); - utilBrowserOpen(url); - result->Success(); - } - else { - result->NotImplemented(); - } - }); -} -#undef RETURN_BADARG_ERR - -void InitEventChannel(flutter::FlutterEngine* flutter_instance) { - eventHandler = std::make_unique( - std::string("net.seven.nodebase/event"), flutter_instance); -} -#endif // _FLUTTER_DART_ \ No newline at end of file diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp deleted file mode 100755 index 5015640..0000000 --- a/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,82 +0,0 @@ -// XXX ugly include, what a pity is windows.h uses winsock.h by default -// should define winsock2.h before windows.h -#include -#include -#include -#pragma comment(lib, "Ws2_32.lib") -#pragma comment(lib, "IPHLPAPI.lib") -#include -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" -#include "flutter_dart.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - InitMethodChannel(flutter_controller_->engine()); - InitEventChannel(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - // Flutter can complete the first frame before the "show window" callback is - // registered. The following call ensures a frame is pending to ensure the - // window is shown. It is a no-op if the first frame hasn't completed yet. - flutter_controller_->ForceRedraw(); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h deleted file mode 100755 index 28c2383..0000000 --- a/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp deleted file mode 100755 index 30682ea..0000000 --- a/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(360, 720); - if (!window.Create(L"nodebase", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/windows/runner/resource.h b/windows/runner/resource.h deleted file mode 100755 index ddc7f3e..0000000 --- a/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico deleted file mode 100755 index c04e20c..0000000 Binary files a/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest deleted file mode 100755 index 157e871..0000000 --- a/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - - - - - - - diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp deleted file mode 100755 index 968e5ee..0000000 --- a/windows/runner/utils.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); - std::string utf8_string; - if (target_length <= 0 || target_length > utf8_string.max_size()) { - return utf8_string; - } - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} - -std::wstring Utf8ToUtf16(const char* utf8_string) { - // ref: filled by chatGPT 4 turbo - if (utf8_string == nullptr) { - return std::wstring(); // Return an empty wstring if the input is null. - } - - // Calculate the length of the resulting wide string. - int wide_char_length = MultiByteToWideChar( - CP_UTF8, // Source string is in UTF-8 - 0, // No flags - utf8_string, // Source UTF-8 string - -1, // The string is null-terminated - nullptr, // No output buffer since we're calculating the length - 0 // Request length calculation - ); - - if (wide_char_length == 0) { - // Handle the error, could be due to an invalid UTF-8 sequence. - // GetLastError() can be used to get more information. - return std::wstring(); - } - - // Allocate a buffer for the wide string. - std::wstring utf16_string(wide_char_length, L'\0'); - - // Now convert the UTF-8 string to UTF-16. - int convert_result = MultiByteToWideChar( - CP_UTF8, // Source string is in UTF-8 - 0, // No flags - utf8_string, // Source UTF-8 string - -1, // The string is null-terminated - &utf16_string[0], // Output buffer for the wide string - wide_char_length // Size of the output buffer - ); - - if (convert_result == 0) { - // Handle the error, could be due to an invalid UTF-8 sequence. - // GetLastError() can be used to get more information. - return std::wstring(); - } - - // The length includes the null terminator, so we resize to remove it. - utf16_string.resize(wide_char_length - 1); - - return utf16_string; -} \ No newline at end of file diff --git a/windows/runner/utils.h b/windows/runner/utils.h deleted file mode 100755 index 13e37c8..0000000 --- a/windows/runner/utils.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); -std::wstring Utf8ToUtf16(const char* utf8_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp deleted file mode 100755 index 1b2adca..0000000 --- a/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "win32_window.h" - -#include -#include - -#include "resource.h" - -namespace { - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registrar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW ^ (WS_THICKFRAME | WS_MAXIMIZEBOX), - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - UpdateTheme(window); - - return OnCreate(); -} - -bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, - RRF_RT_REG_DWORD, nullptr, &light_mode, - &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h deleted file mode 100755 index 49b847f..0000000 --- a/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates a win32 window with |title| that is positioned and sized using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_