From 3eb76d80d9f1ac35873b6e6efea72f13f0160656 Mon Sep 17 00:00:00 2001 From: Linloir <3145078758@qq.com> Date: Sun, 9 Oct 2022 00:01:17 +0800 Subject: [PATCH] initial commit --- .gitignore | 6 + CHANGELOG.md | 3 + README.md | 2 + analysis_options.yaml | 30 + bin/tcp_server.dart | 156 +++++ lib/database.dart | 775 ++++++++++++++++++++++++ lib/requesthandler.dart | 272 +++++++++ lib/tcpcontroller/controller.dart | 125 ++++ lib/tcpcontroller/payload/identity.dart | 26 + lib/tcpcontroller/payload/message.dart | 59 ++ lib/tcpcontroller/payload/userinfo.dart | 26 + lib/tcpcontroller/request.dart | 51 ++ lib/tcpcontroller/response.dart | 53 ++ lib/utils/typeconverter.dart | 22 + pubspec.lock | 369 +++++++++++ pubspec.yaml | 21 + 16 files changed, 1996 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 bin/tcp_server.dart create mode 100644 lib/database.dart create mode 100644 lib/requesthandler.dart create mode 100644 lib/tcpcontroller/controller.dart create mode 100644 lib/tcpcontroller/payload/identity.dart create mode 100644 lib/tcpcontroller/payload/message.dart create mode 100644 lib/tcpcontroller/payload/userinfo.dart create mode 100644 lib/tcpcontroller/request.dart create mode 100644 lib/tcpcontroller/response.dart create mode 100644 lib/utils/typeconverter.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c8a157 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build output. +build/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3816eca --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/bin/tcp_server.dart b/bin/tcp_server.dart new file mode 100644 index 0000000..c5f9c2f --- /dev/null +++ b/bin/tcp_server.dart @@ -0,0 +1,156 @@ +/* + * @Author : Linloir + * @Date : 2022-10-06 15:44:16 + * @LastEditTime : 2022-10-08 23:57:37 + * @Description : + */ + +import 'dart:convert'; +import 'dart:io'; + +import 'package:tcp_server/database.dart'; +import 'package:tcp_server/requesthandler.dart'; +import 'package:tcp_server/tcpcontroller/controller.dart'; +import 'package:tcp_server/tcpcontroller/request.dart'; +import 'package:tcp_server/tcpcontroller/response.dart'; + +void main(List arguments) async { + await DataBaseHelper().initialize(); + var tokenMap = {}; + var socketMap = >{}; + var listenSocket = await ServerSocket.bind('127.0.0.1', 20706); + listenSocket.listen( + (socket) { + var controller = TCPController(socket: socket); + controller.stream.listen((request) async { + if(request.tokenID == null) { + if(socketMap[socket] == null) { + socketMap[socket] = (() async => (await DataBaseHelper().createToken()))(); + } + request.tokenID = await socketMap[socket]; + var tokenResponse = TCPResponse( + type: RequestType.token, + status: ResponseStatus.ok, + body: { + "tokenid": request.tokenID + } + ); + await socket.addStream(tokenResponse.stream); + } + tokenMap[request.tokenID!] = tokenMap[request.tokenID!] ?? socket; + switch(request.requestType) { + case RequestType.checkState: { + var response = await onCheckState(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.register: { + var response = await onRegister(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.login: { + var response = await onLogin(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.logout: { + var response = await onLogout(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.profile: { + var response = await onFetchProfile(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.modifyProfile: { + var response = await onModifyProfile(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.modifyPassword: { + var response = await onModifyPassword(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.sendMessage: { + //Forword Message + var message = request.body['message'] as Map; + await DataBaseHelper().setFetchHistoryFor( + tokenID: request.tokenID, + newTimeStamp: message['timestamp'] as int + ); + var originUserID = message['userid'] as int; + var onlineDevices = await DataBaseHelper().fetchTokenIDsViaUserID(userID: originUserID); + for(var device in onlineDevices) { + if(device == request.tokenID) { + continue; + } + var targetSocket = tokenMap[device]; + targetSocket?.write(jsonEncode({ + 'response': 'FORWARDMSG', + 'body': { + "message": message + } + })); + //Update Fetch Histories + await DataBaseHelper().setFetchHistoryFor( + tokenID: device, + newTimeStamp: message['timestamp'] as int + ); + } + var targetUserID = message['targetid'] as int; + var targetDevices = await DataBaseHelper().fetchTokenIDsViaUserID(userID: targetUserID); + for(var device in targetDevices) { + //Forward to socket + var targetSocket = tokenMap[device]; + targetSocket?.write(jsonEncode({ + 'response': 'FORWARDMSG', + 'body': { + "message": message + } + })); + //Update Fetch Histories + await DataBaseHelper().setFetchHistoryFor( + tokenID: device, + newTimeStamp: message['timestamp'] as int + ); + } + var response = await onSendMessage(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.fetchMessage: { + var response = await onFetchMessage(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.fetchFile: { + var response = await onFetchFile(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.searchUser: { + var response = await onSearchUser(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.fetchContact: { + var response = await onFetchContact(request, socket); + await socket.addStream(response.stream); + break; + } + case RequestType.unknown: { + var response = await onUnknownRequest(request, socket); + await socket.addStream(response.stream); + break; + } + default: { + print('[E] Drop out of switch case'); + } + } + }); + }, + ); +} diff --git a/lib/database.dart b/lib/database.dart new file mode 100644 index 0000000..72c21ae --- /dev/null +++ b/lib/database.dart @@ -0,0 +1,775 @@ +/* + * @Author : Linloir + * @Date : 2022-10-06 16:15:01 + * @LastEditTime : 2022-10-08 23:54:36 + * @Description : + */ + +import 'dart:io'; + +import 'package:sqflite_common/sqlite_api.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:tcp_server/tcpcontroller/payload/identity.dart'; +import 'package:tcp_server/tcpcontroller/payload/message.dart'; +import 'package:tcp_server/tcpcontroller/payload/userinfo.dart'; + +class DataBaseHelper { + static final DataBaseHelper _helper = DataBaseHelper._internal(); + late final Database _database; + + factory DataBaseHelper() { + return _helper; + } + + DataBaseHelper._internal(); + + Future initialize() async { + _database = await databaseFactoryFfi.openDatabase( + 'E:\\database.db', + options: OpenDatabaseOptions( + version: 1, + onCreate: (db, version) { + print('[L] Creating Database.'); + db.execute( + ''' + CREATE TABLE users ( + userid integer primary key autoincrement, + username text not null, + passwd text not null, + avatar text + ); + create table msgs ( + userid integer, + targetid integer, + contenttype text not null, + content text not null, + timestamp integer, + md5encoded text primary key not null + ); + create table contacts ( + userid integer, + targetid integer, + primary key (userid, targetid) + ); + create table tokens ( + tokenid integer primary key autoincrement, + createtime integer not null, + lastused integer not null + ); + create table bindings ( + tokenid integer primary key, + userid integer + ); + create table histories ( + tokenid integer, + userid integer, + lastfetch integer not null, + primary key (tokenid, userid) + ); + create table files ( + filemd5 text primary key not null, + dir text not null + ); + create table msgfiles ( + msgmd5 text not null, + filemd5 text not null, + primary key (msgmd5, filemd5) + ); + ''' + ); + }, + ) + ); + } + + //Creates new token + Future createToken() async { + //Insert new row + var row = await _database.rawInsert( + ''' + insert into tokens(createtime, lastused) + values (?, ?) + ''', + [ + DateTime.now().millisecondsSinceEpoch, + DateTime.now().millisecondsSinceEpoch + ] + ); + //Fetch new row + var newToken = (await _database.query( + 'tokens', + where: 'rowid = $row', + ))[0]['tokenid'] as int; + //Return token + return newToken; + } + + Future checkLoginState({ + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + var bindingQueryResult = await _database.query( + 'bindings natural join users', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ); + if(bindingQueryResult.isNotEmpty) { + return UserInfo( + userID: bindingQueryResult[0]['userid'] as int, + userName: bindingQueryResult[0]['username'] as String, + userAvatar: bindingQueryResult[0]['avatar'] as String? + ); + } + else { + throw Exception('User not logged in'); + } + } + + Future logIn({ + required UserIdentity identity, + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + var userIdentities = await _database.query( + 'users', + where: 'username = ?', + whereArgs: [ + identity.userName + ] + ); + if(userIdentities.isNotEmpty) { + var user = userIdentities[0]; + if(user['passwd'] == identity.userPasswd) { + //Query for existed token binding + var existBindings = await _database.query( + 'bindings', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ); + if(existBindings.isEmpty) { + //Add new binding + await _database.insert( + 'bindings', + { + 'tokenid': tokenID, + 'userid': user['userid'] + } + ); + } + else { + //Update token binding + await _database.update( + 'bindings', + { + 'tokenid': tokenID, + 'userid': user['userid'] + }, + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ); + } + return UserInfo( + userID: user['userid'] as int, + userName: user['username'] as String, + userAvatar: user['avatar'] as String? + ); + } + else { + throw Exception('Invalid password'); + } + } + else { + throw Exception('User not found'); + } + } + + Future logOut({ + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Delete binding + await _database.delete( + 'bindings', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ); + } + + Future registerUser({ + required UserIdentity identity, + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Insert into users + try { + await _database.insert( + 'users', + { + 'username': identity.userName, + 'passwd': identity.userPasswd, + 'avatar': null + }, + conflictAlgorithm: ConflictAlgorithm.rollback + ); + } catch (conflict) { + throw Exception(['Database failure', conflict.toString()]); + } + + //Get new userid + var newUserID = (await _database.query( + 'users', + where: 'username = ?', + whereArgs: [ + identity.userName + ] + ))[0]['userid'] as int; + + //Insert into bindings + await _database.insert( + 'bindings', + { + 'tokenid': tokenID, + 'userid': newUserID + } + ); + + return UserInfo( + userID: newUserID, + userName: identity.userName, + userAvatar: null + ); + } + + Future modifyUserPassword({ + required UserIdentity newIdentity, + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Find current binded user + var currentUserQueryResult = await _database.query( + 'bindings natural join users', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ); + if(currentUserQueryResult.isEmpty) { + throw Exception('User not logged in'); + } + var currentUser = currentUserQueryResult[0]; + + //Verify user identity + if(currentUser['passwd'] as String != newIdentity.userPasswd) { + throw Exception('Wrong password'); + } + else { + try { + //Modify database + await _database.update( + 'users', + { + 'passwd': newIdentity.userPasswdNew + }, + where: 'userid = ${currentUser['userid'] as int}', + conflictAlgorithm: ConflictAlgorithm.rollback + ); + } catch (conflict) { + throw Exception(['Database failure', conflict.toString()]); + } + } + } + + //Returns a list of unfetched messages in JSON format + Future> fetchMessagesFor({ + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Find userID and last fetched time + var userIdQueryResult = await _database.query( + 'bindings natural left outer join histories', + columns: ['userid', 'lastfetch'], + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ); + if(userIdQueryResult.isEmpty) { + throw Exception('User not logged in'); + } + var userID = userIdQueryResult[0]['userid'] as int; + var lastFetch = userIdQueryResult[0]['lastfetch'] as int?; + if(lastFetch == null) { + //First fetch, add to fetch history + await _database.insert( + 'histories', + { + 'tokenid': tokenID, + 'userid': userID, + 'lastfetch': 0 + } + ); + lastFetch = 0; + } + + //Fetch unfetched messages + var unfetchMsgQueryResult = await _database.query( + 'msgs', + where: '(userid = ? or targetid = ?) and timestamp > ?', + whereArgs: [ + userID, + userID, + lastFetch + ], + orderBy: 'timestamp desc' + ); + var unfetchMessages = unfetchMsgQueryResult.map((message) { + return Message( + userid: message['userid'] as int, + targetid: message['targetid'] as int, + contenttype: MessageType.fromStringLiteral( + message['contenttype'] as String + ), + content: message['content'] as String, + timestamp: message['timestamp'] as int, + md5encoded: message['md5encoded'] as String + ); + }).toList(); + + //Set new fetch history + if(unfetchMsgQueryResult.isNotEmpty) { + await _database.update( + 'histories', + { + 'lastfetch': unfetchMsgQueryResult[0]['timestamp'] + }, + where: 'tokenid = ? and userid = ?', + whereArgs: [ + tokenID, + userID + ] + ); + } + + //return result + return unfetchMessages; + } + + Future setFetchHistoryFor({ + required int? tokenID, + required int newTimeStamp + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Get current userid + var bindingQueryResult = await _database.query( + 'bindings natural left outer join histories', + where: 'bindings.tokenid = ?', + whereArgs: [ + tokenID + ] + ); + if(bindingQueryResult.isEmpty) { + //Be silence on err + return; + } + var userID = bindingQueryResult[0]['userid'] as int; + + //Check for fetch history + var lastFetch = bindingQueryResult[0]['lastfetch'] as int?; + if(lastFetch == null) { + //First fetch, add to fetch history + await _database.insert( + 'histories', + { + 'tokenid': tokenID, + 'userid': userID, + 'lastfetch': newTimeStamp + } + ); + } + else { + //Update fetch history + await _database.update( + 'histories', + { + 'lastfetch': newTimeStamp + }, + where: 'tokenid = ? and userid = ?', + whereArgs: [ + tokenID, + userID + ] + ); + } + } + + Future storeMessage({ + required Message msg, + String? fileMd5 + }) async { + try { + await _database.insert( + 'msgs', + { + 'userid': msg.senderID, + 'targetid': msg.receiverID, + 'contenttype': msg.contentType.literal, + 'content': msg.content, + 'timestamp': msg.timestamp, + 'md5encoded': msg.md5encoded, + } + ); + } catch (err) { + print('[E] Database failure on message storage:'); + print('[>] $err'); + } + if(msg.contentType == MessageType.file) { + if(fileMd5 == null) { + await _database.delete( + 'msgs', + where: 'md5encoded = ?', + whereArgs: [ + msg.md5encoded + ] + ); + throw Exception('Missing file for message'); + } + await _database.insert( + 'msgfiles', + { + 'msgmd5': msg.md5encoded, + 'filemd5': fileMd5 + } + ); + } + } + + Future storeFile({ + required File? tempFile, + required String? fileMd5 + }) async { + if(tempFile == null || fileMd5 == null) { + throw Exception('Missing file parts'); + } + var filePath = '${Directory.current.path}\\$fileMd5'; + await tempFile.copy(filePath); + tempFile.delete(); + try { + await _database.insert( + 'files', + { + 'filemd5': fileMd5, + 'dir': filePath + }, + conflictAlgorithm: ConflictAlgorithm.rollback + ); + } catch (conflict) { + throw Exception(['Database failure', conflict.toString()]); + } + } + + Future fetchFilePath({ + required String msgMd5 + }) async { + var queryResult = await _database.query( + 'msgfiles natural join files', + where: 'msgfile.msgmd5 = ?', + whereArgs: [ + msgMd5 + ] + ); + if(queryResult.isEmpty) { + throw Exception('File not found'); + } + return queryResult[0]['filedir'] as String; + } + + Future fetchUserInfoViaToken({ + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Find current binded userID + var currentUserQueryResult = (await _database.query( + 'bindings natural join users', + where: 'bindings.tokenid = ?', + whereArgs: [ + tokenID + ] + )); + if(currentUserQueryResult.isEmpty) { + throw Exception('User not logged in'); + } + var currentUser = currentUserQueryResult[0]; + + return UserInfo( + userID: currentUser['userid'] as int, + userName: currentUser['username'] as String, + userAvatar: currentUser['avatar'] as String? + ); + } + + Future modifyUserInfo({ + required UserInfo userInfo, + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Find current binded userID + var currentUserIDQueryResult = (await _database.query( + 'bindings', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + )); + if(currentUserIDQueryResult.isEmpty) { + throw Exception('User not logged in'); + } + var currentUserID = currentUserIDQueryResult[0]['userid'] as int; + + //Update database + try { + await _database.update( + 'users', + { + 'username': userInfo.userName, + 'avatar': userInfo.userAvatar + }, + conflictAlgorithm: ConflictAlgorithm.rollback + ); + } catch (conflict) { + throw Exception(['Database failure', conflict.toString()]); + } + + //Return result + return UserInfo( + userID: currentUserID, + userName: userInfo.userName, + userAvatar: userInfo.userAvatar + ); + } + + Future fetchUserInfoViaUsername({ + required String username + }) async { + var targetUserQueryResult = await _database.query( + 'users', + columns: [ + 'userid', + 'username', + 'avatar' + ], + where: 'username = ?', + whereArgs: [ + username + ] + ); + if(targetUserQueryResult.isNotEmpty) { + return UserInfo( + userID: targetUserQueryResult[0]['userid'] as int, + userName: targetUserQueryResult[0]['username'] as String, + userAvatar: targetUserQueryResult[0]['avatar'] as String? + ); + } + else { + throw Exception('User not found'); + } + } + + Future> fetchContact({ + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Find current binded userID + var currentUserID = (await _database.query( + 'bindings', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ))[0]['userid'] as int; + + //Fetch all contacts + var contactsQueryResult = await _database.query( + 'contacts as I join contacts as P on I.targetid = P.userid join users on I.targetid = users.userid', + columns: ['I.targetid as userid', 'users.username as username', 'users.avatar as avatar'], + where: 'I.userid = P.targetid and I.userid = ?', + whereArgs: [ + currentUserID + ] + ); + + //Convert to encodable objects + var contactsEncodable = contactsQueryResult.map((contact) { + return UserInfo( + userID: contact['userid'] as int, + userName: contact['username'] as String, + userAvatar: contact['avatar'] as String? + ); + }).toList(); + + return contactsEncodable; + } + + Future> fetchPendingContacts({ + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Find current binded userID + var currentUserID = (await _database.query( + 'bindings', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ))[0]['userid'] as int; + + //Fetch pending contacts + var contactsQueryResult = await _database.query( + 'contacts join users on contacts.targetid = users.userid', + columns: ['contacts.targetid as userid', 'users.username as username', 'users.avatar as avatar'], + where: '''contacts.userid = ? and not exists ( + select * from contacts as S + where contacts.targetid = S.userid and contacts.userid = S.targetid + )''', + whereArgs: [ + currentUserID + ] + ); + + //Convert to encodable objects + var contactsEncodable = contactsQueryResult.map((contact) { + return UserInfo( + userID: contact['userid'] as int, + userName: contact['username'] as String, + userAvatar: contact['avatar'] as String? + ); + }).toList(); + + return contactsEncodable; + } + + Future> fetchRequestingContacts({ + required int? tokenID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Find current binded userID + var currentUserID = (await _database.query( + 'bindings', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ))[0]['userid'] as int; + + //Fetch pending contacts + var contactsQueryResult = await _database.query( + 'contacts join users on contacts.userid = users.userid', + columns: ['contacts.userid as userid', 'users.username as username', 'users.avatar as avatar'], + where: '''contacts.targetid = ? and not exists ( + select * from contacts as S + where contacts.targetid = S.userid and contacts.userid = S.targetid + )''', + whereArgs: [ + currentUserID + ] + ); + + //Convert to encodable objects + var contactsEncodable = contactsQueryResult.map((contact) { + return UserInfo( + userID: contact['userid'] as int, + userName: contact['username'] as String, + userAvatar: contact['avatar'] as String? + ); + }).toList(); + + return contactsEncodable; + } + + Future addContact({ + required int? tokenID, + required int userID + }) async { + if(tokenID == null) { + throw Exception('Invalid device token'); + } + + //Find current binded userID + var currentUserID = (await _database.query( + 'bindings', + where: 'tokenid = ?', + whereArgs: [ + tokenID + ] + ))[0]['userid'] as int; + + //Add contacts + await _database.insert( + 'contacts', + { + 'userid': currentUserID, + 'targetid': userID + }, + conflictAlgorithm: ConflictAlgorithm.ignore + ); + } + + Future> fetchTokenIDsViaUserID({ + required int userID + }) async { + var tokenIDQueryResult = await _database.query( + 'bindings', + where: 'userid = ?', + whereArgs: [ + userID + ] + ); + + return tokenIDQueryResult.map((token) { + return token['tokenid'] as int; + }).toList(); + } +} diff --git a/lib/requesthandler.dart b/lib/requesthandler.dart new file mode 100644 index 0000000..2774f9c --- /dev/null +++ b/lib/requesthandler.dart @@ -0,0 +1,272 @@ +/* + * @Author : Linloir + * @Date : 2022-10-08 20:52:48 + * @LastEditTime : 2022-10-08 23:39:59 + * @Description : + */ + +import 'dart:io'; + +import 'package:tcp_server/database.dart'; +import 'package:tcp_server/tcpcontroller/payload/identity.dart'; +import 'package:tcp_server/tcpcontroller/payload/message.dart'; +import 'package:tcp_server/tcpcontroller/payload/userinfo.dart'; +import 'package:tcp_server/tcpcontroller/request.dart'; +import 'package:tcp_server/tcpcontroller/response.dart'; + +Future onCheckState(TCPRequest request, Socket socket) async { + try { + var userInfo = await DataBaseHelper().checkLoginState(tokenID: request.tokenID); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + body: userInfo.jsonObject + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onRegister(TCPRequest request, Socket socket) async { + try { + UserIdentity identity = UserIdentity.fromJSONObject(request.body); + var newUserInfo = await DataBaseHelper().registerUser( + identity: identity, + tokenID: request.tokenID + ); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + body: newUserInfo.jsonObject + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onLogin(TCPRequest request, Socket socket) async { + try { + var userInfo = await DataBaseHelper().logIn( + identity: UserIdentity.fromJSONObject(request.body), + tokenID: request.tokenID + ); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + body: userInfo.jsonObject + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onLogout(TCPRequest request, Socket socket) async { + try { + await DataBaseHelper().logOut(tokenID: request.tokenID); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onFetchProfile(TCPRequest request, Socket socket) async { + try { + var userInfo = await DataBaseHelper().fetchUserInfoViaToken(tokenID: request.tokenID); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + body: userInfo.jsonObject + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onModifyPassword(TCPRequest request, Socket socket) async { + try { + await DataBaseHelper().modifyUserPassword( + newIdentity: UserIdentity.fromJSONObject(request.body), + tokenID: request.tokenID + ); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onModifyProfile(TCPRequest request, Socket socket) async { + try { + var newUserInfo = await DataBaseHelper().modifyUserInfo( + userInfo: UserInfo.fromJSONObject(request.body), + tokenID: request.tokenID + ); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + body: newUserInfo.jsonObject + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onSendMessage(TCPRequest request, Socket socket) async { + try { + var message = Message.fromJSONObject(request.body); + if(message.contentType == MessageType.file) { + await DataBaseHelper().storeFile( + tempFile: request.payload, + fileMd5: message.fileMd5 + ); + } + await DataBaseHelper().storeMessage( + msg: message, + fileMd5: message.fileMd5 + ); + return TCPResponse( + type: RequestType.sendMessage, + status: ResponseStatus.ok, + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onFetchMessage(TCPRequest request, Socket socket) async { + try { + var messages = await DataBaseHelper().fetchMessagesFor(tokenID: request.tokenID); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + body: { + 'messages': messages.map((e) => e.jsonObject) + } + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onFetchFile(TCPRequest request, Socket socket) async { + try { + var filePath = await DataBaseHelper().fetchFilePath(msgMd5: request.body['msgmd5'] as String); + var file = File(filePath); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + payload: file + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onSearchUser(TCPRequest request, Socket socket) async { + try { + var userInfo = await DataBaseHelper().fetchUserInfoViaUsername(username: request.body['username'] as String); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + body: userInfo.jsonObject + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onAddContact(TCPRequest request, Socket socket) async { + try { + await DataBaseHelper().addContact(tokenID: request.tokenID, userID: request.body['userid'] as int); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onFetchContact(TCPRequest request, Socket socket) async { + try { + var contacts = await DataBaseHelper().fetchContact(tokenID: request.tokenID); + var pendingContacts = await DataBaseHelper().fetchPendingContacts(tokenID: request.tokenID); + var requestingContacts = await DataBaseHelper().fetchRequestingContacts(tokenID: request.tokenID); + return TCPResponse( + type: request.requestType, + status: ResponseStatus.ok, + body: { + "contacts": contacts.map((e) => e.jsonObject), + "pending": pendingContacts.map((e) => e.jsonObject), + "requesting": requestingContacts.map((e) => e.jsonObject) + } + ); + } on Exception catch (exception) { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: exception.toString() + ); + } +} + +Future onUnknownRequest(TCPRequest request, Socket socket) async { + return TCPResponse( + type: request.requestType, + status: ResponseStatus.err, + errInfo: 'Unkown request' + ); +} \ No newline at end of file diff --git a/lib/tcpcontroller/controller.dart b/lib/tcpcontroller/controller.dart new file mode 100644 index 0000000..642666d --- /dev/null +++ b/lib/tcpcontroller/controller.dart @@ -0,0 +1,125 @@ +/* + * @Author : Linloir + * @Date : 2022-10-08 15:10:04 + * @LastEditTime : 2022-10-08 23:11:24 + * @Description : + */ + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:tcp_server/tcpcontroller/request.dart'; + +class TCPController { + final Socket socket; + + //Stores the incoming bytes of the TCP connection temporarily + final Uint8List buffer = Uint8List(0); + + //Byte length for json object + int requestLength = 0; + //Byte length for subsequent data of the json object + int payloadLength = 0; + + //Construct a stream which emits events on intact requests + StreamController _requestStreamController = StreamController()..close(); + + //Construct a payload stream which forward the incoming byte into temp file + StreamController _payloadStreamController = StreamController()..close(); + + //Provide a request stream for caller functions to listen on + final StreamController _streamController = StreamController(); + Stream get stream => _streamController.stream; + + TCPController({ + required this.socket + }) { + socket.listen(socketHandler); + } + + //Listen to the incoming stream and emits event whenever there is a intact request + void socketHandler(Uint8List fetchedData) { + //Put incoming data into buffer + buffer.addAll(fetchedData); + //Consume buffer until it's not enough for first 8 byte of a message + while(true) { + if(requestLength == 0 && payloadLength == 0) { + //New request + if(buffer.length > 8) { + //Buffered data has more than 8 bytes, enough to read request length and body length + requestLength = buffer.sublist(0, 4).buffer.asByteData().getInt32(0); + payloadLength = buffer.sublist(4, 8).buffer.asByteData().getInt32(0); + //Clear the length indicator bytes + buffer.removeRange(0, 8); + //Create temp file to read payload (might be huge) + var tempFile = File('./temp${DateTime.now().microsecondsSinceEpoch}.temp')..createSync(); + //Initialize payload transmission controller + _payloadStreamController = StreamController(); + //Bind file to stream + _payloadStreamController.stream.listen((data) { + tempFile.writeAsBytes(data, mode: FileMode.append); + }); + //Bind request construction on stream + _requestStreamController = StreamController(); + _requestStreamController.stream.listen((requestBytes) { + //When request stream is closed by controller + var request = TCPRequest(requestBytes, tempFile); + _payloadStreamController.done.then((_) { + _streamController.add(request); + }); + }); + } + else { + //Buffered data is not long enough + //Do nothing + break; + } + } + else { + //Currently awaiting full transmission + if(requestLength > 0) { + //Currently processing on a request + if(buffer.length > requestLength) { + //Got intact request json + //Emit request buffer through stream + _requestStreamController.add(buffer.sublist(0, requestLength)); + _requestStreamController.close(); + //Remove proccessed buffer + buffer.removeRange(0, requestLength); + //Clear awaiting request length + requestLength = 0; + } + else { + //Got part of request json + //do nothing + break; + } + } + else { + //Currently processing on a payload + if(buffer.length >= payloadLength) { + //Last few bytes to emit + //Send the last few bytes to stream + _payloadStreamController.add(buffer.sublist(0, payloadLength)); + //Clear buffer + buffer.removeRange(0, payloadLength); + //Set payload length to zero + payloadLength = 0; + //Close the payload transmission stream + _payloadStreamController.close(); + } + else { + //Part of payload + //Transmit all to stream + _payloadStreamController.add(buffer); + //Reduce payload bytes left + payloadLength -= buffer.length; + //Clear buffer + buffer.clear(); + } + } + } + } + } +} \ No newline at end of file diff --git a/lib/tcpcontroller/payload/identity.dart b/lib/tcpcontroller/payload/identity.dart new file mode 100644 index 0000000..9ab2761 --- /dev/null +++ b/lib/tcpcontroller/payload/identity.dart @@ -0,0 +1,26 @@ +/* + * @Author : Linloir + * @Date : 2022-10-08 16:16:10 + * @LastEditTime : 2022-10-08 22:36:19 + * @Description : + */ + +class UserIdentity { + final Map _data; + + UserIdentity({ + required String userName, + required String userPasswdEncoded, + String? userPasswdEncodedNew + }): _data = { + "username": userName, + "passwd": userPasswdEncoded, + "newPasswd": userPasswdEncodedNew + }; + UserIdentity.fromJSONObject(Map data): _data = data; + + String get userName => _data['username'] as String; + String get userPasswd => _data['passwd'] as String; + String? get userPasswdNew => _data['newPasswd'] as String?; + Map get jsonObject => _data; +} \ No newline at end of file diff --git a/lib/tcpcontroller/payload/message.dart b/lib/tcpcontroller/payload/message.dart new file mode 100644 index 0000000..c226f8d --- /dev/null +++ b/lib/tcpcontroller/payload/message.dart @@ -0,0 +1,59 @@ +/* + * @Author : Linloir + * @Date : 2022-10-08 16:16:19 + * @LastEditTime : 2022-10-08 23:20:36 + * @Description : Message Info Payload + */ + +import 'package:crypto/crypto.dart'; +import 'package:tcp_server/utils/typeconverter.dart'; + +enum MessageType { + plaintext('plaintext'), + file('file'), + image('image'); + + factory MessageType.fromStringLiteral(String value) { + return MessageType.values.firstWhere((element) => element._value == value); + } + const MessageType(String value): _value = value; + final String _value; + String get literal => _value; +} + +class Message { + final Map _data; + + Message({ + required int userid, + required int targetid, + required MessageType contenttype, + required String content, + required int timestamp, + String? md5encoded, + String? filemd5 + }): _data = { + "userid": userid, + "targetid": targetid, + "contenttype": contenttype.literal, + "content": content, + "timestamp": timestamp, + "md5encoded": md5encoded ?? md5.convert( + intToUint8List(userid) + ..addAll(intToUint8List(targetid)) + ..addAll(intToUint8List(timestamp)) + ..addAll(content.codeUnits) + ), + "filemd5": filemd5 + }; + Message.fromJSONObject(Map data): _data = data; + + int get senderID => _data['userid'] as int; + int get receiverID => _data['targetid'] as int; + MessageType get contentType => MessageType.fromStringLiteral(_data['contenttype'] as String); + String get content => _data['content'] as String; + int get timestamp => _data['timestamp'] as int; + String get md5encoded => _data['md5encoded'] as String; + String? get fileMd5 => _data['filemd5'] as String?; + Map get jsonObject => _data; +} \ No newline at end of file diff --git a/lib/tcpcontroller/payload/userinfo.dart b/lib/tcpcontroller/payload/userinfo.dart new file mode 100644 index 0000000..1ac8e43 --- /dev/null +++ b/lib/tcpcontroller/payload/userinfo.dart @@ -0,0 +1,26 @@ +/* + * @Author : Linloir + * @Date : 2022-10-08 16:15:17 + * @LastEditTime : 2022-10-08 22:35:53 + * @Description : User Info Payload + */ + +class UserInfo { + final Map _data; + + UserInfo({ + required int userID, + required String userName, + String? userAvatar + }): _data = { + "userid": userID, + "username": userName, + "avatar": userAvatar + }; + UserInfo.fromJSONObject(Map data): _data = data; + + int get userID => _data['userid'] as int; + String get userName => _data['username'] as String; + String? get userAvatar => _data['avatar'] as String?; + Map get jsonObject => _data; +} \ No newline at end of file diff --git a/lib/tcpcontroller/request.dart b/lib/tcpcontroller/request.dart new file mode 100644 index 0000000..e87ffa1 --- /dev/null +++ b/lib/tcpcontroller/request.dart @@ -0,0 +1,51 @@ +/* + * @Author : Linloir + * @Date : 2022-10-08 15:14:26 + * @LastEditTime : 2022-10-08 23:52:50 + * @Description : + */ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +enum RequestType { + token ('TOKEN'), //Only exists when server is sending message + checkState ('STATE'), //Check login state for device token + register ('REGISTER'), //Register new user + login ('LOGIN'), //Login via username and password + logout ('LOGOUT'), //Logout for current device token + profile ('PROFILE'), //Fetch current logged in user profile + modifyPassword('MODIFYPASSWD'), //Modify user password + modifyProfile ('MODIFYPROFILE'), //Modify user profile + sendMessage ('SENDMSG'), //Send message + fetchMessage ('FETCHMSG'), //Fetch message + findFile ('FINDFILE'), //Find file by md5 before transmitting the file + fetchFile ('FETCHFILE'), //Fetch file and file md5 by message md5 + searchUser ('SEARCHUSR'), //Search username and userid by username + addContact ('ADDCONTACT'), //Add one-way relation to a user + fetchContact ('FETCHCONTACT'), //Fetch all contacts, including requesting and pending + unknown ('UNKNOWN'); //Wrong command + + const RequestType(String value): _value = value; + final String _value; + String get value => _value; + + //Construct the enum type by value + factory RequestType.fromValue(String value) { + return RequestType.values.firstWhere((element) => element._value == value, orElse: () => RequestType.unknown); + } +} + +//Object wrapper for tcp request string +class TCPRequest { + final Map _data; + File? payload; + + TCPRequest(Uint8List data, this.payload): _data = jsonDecode(String.fromCharCodes(data)); + + String get toJSON => jsonEncode(_data); + RequestType get requestType => RequestType.fromValue(_data['request'] as String); + int? get tokenID => _data['tokenid'] as int?; + set tokenID(int? t) => _data['tokenid'] = t; + Map get body => _data['body'] as Map? ?? {}; +} \ No newline at end of file diff --git a/lib/tcpcontroller/response.dart b/lib/tcpcontroller/response.dart new file mode 100644 index 0000000..aac21cd --- /dev/null +++ b/lib/tcpcontroller/response.dart @@ -0,0 +1,53 @@ +/* + * @Author : Linloir + * @Date : 2022-10-08 22:40:47 + * @LastEditTime : 2022-10-08 23:05:01 + * @Description : + */ + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:tcp_server/tcpcontroller/request.dart'; + +enum ResponseStatus { + ok('OK'), + err('ERR'); + + final String _value; + const ResponseStatus(String v): _value = v; + + String get value => _value; +} + +class TCPResponse { + final String responseJson; + final File? payloadFile; + + TCPResponse({ + required RequestType type, + required ResponseStatus status, + Map? body, + String? errInfo, + File? payload + }): + responseJson = jsonEncode({ + "response": type.value, + "status": status.value, + "info": errInfo, + "body": body, + }), + payloadFile = payload; + + int get responseLength => responseJson.length; + int get payloadLength => payloadFile?.lengthSync() ?? 0; + Stream get stream async* { + yield Uint8List(4)..buffer.asInt32List()[0] = responseLength; + yield Uint8List(4)..buffer.asInt32List()[0] = payloadLength; + yield Uint8List.fromList(responseJson.codeUnits); + if(payloadFile != null) { + yield await payloadFile!.readAsBytes(); + } + } +} \ No newline at end of file diff --git a/lib/utils/typeconverter.dart b/lib/utils/typeconverter.dart new file mode 100644 index 0000000..308ca1f --- /dev/null +++ b/lib/utils/typeconverter.dart @@ -0,0 +1,22 @@ +/* + * @Author : Linloir + * @Date : 2022-10-08 17:21:45 + * @LastEditTime : 2022-10-08 17:29:21 + * @Description : Type Converters + */ + +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; + +String uint8ListToHexString(Uint8List data) { + return data.buffer.asUint8List().map((e) => e.toRadixString(16).padLeft(2, '0')).join(); +} + +Uint8List hexToUint8List(String string) { + return Uint8List.fromList(hex.decode(string)); +} + +Uint8List intToUint8List(int value) { + return Uint8List(4)..buffer.asInt32List()[0] = value; +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..318f0d2 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,369 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.flutter-io.cn" + source: hosted + version: "49.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + convert: + dependency: "direct main" + description: + name: convert + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.1" + crypto: + dependency: "direct main" + description: + name: crypto + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.4" + lints: + dependency: "direct dev" + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.10.10" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + sqflite_common: + dependency: "direct main" + description: + name: sqflite_common + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1+1" + sqlite3: + dependency: transitive + description: + name: sqlite3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0+3" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.21.6" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.14" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.18" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + udp: + dependency: "direct main" + description: + name: udp + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.3" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.4.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.18.2 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..662b334 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,21 @@ +name: tcp_server +description: A sample command-line application. +version: 1.0.0 +# homepage: https://www.example.com + +environment: + sdk: '>=2.18.2 <3.0.0' + +dependencies: + udp: ^5.0.3 + sqflite_common_ffi: ^2.1.1+1 + sqflite_common: ^2.3.0 + crypto: ^3.0.2 + convert: ^3.0.2 + +# dependencies: +# path: ^1.8.0 + +dev_dependencies: + lints: ^2.0.0 + test: ^1.16.0