initial commit

This commit is contained in:
Linloir 2022-10-09 00:01:17 +08:00
commit 3eb76d80d9
No known key found for this signature in database
GPG Key ID: 58EEB209A0F2C366
16 changed files with 1996 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Files and directories created by pub.
.dart_tool/
.packages
# Conventional directory for build output.
build/

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
A sample command-line application with an entrypoint in `bin/`, library code
in `lib/`, and example unit test in `test/`.

30
analysis_options.yaml Normal file
View File

@ -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

156
bin/tcp_server.dart Normal file
View File

@ -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<String> arguments) async {
await DataBaseHelper().initialize();
var tokenMap = <int, Socket>{};
var socketMap = <Socket, Future<int>>{};
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<String, Object?>;
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');
}
}
});
},
);
}

775
lib/database.dart Normal file
View File

@ -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<void> 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<int> 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<UserInfo> 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<UserInfo> 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<void> logOut({
required int? tokenID
}) async {
if(tokenID == null) {
throw Exception('Invalid device token');
}
//Delete binding
await _database.delete(
'bindings',
where: 'tokenid = ?',
whereArgs: [
tokenID
]
);
}
Future<UserInfo> 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<void> 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<List<Message>> 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<void> 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<void> 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<void> 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<String> 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<UserInfo> 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<UserInfo> 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<UserInfo> 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<List<UserInfo>> 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<List<UserInfo>> 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<List<UserInfo>> 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<void> 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<List<int>> 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();
}
}

272
lib/requesthandler.dart Normal file
View File

@ -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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> 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<TCPResponse> onUnknownRequest(TCPRequest request, Socket socket) async {
return TCPResponse(
type: request.requestType,
status: ResponseStatus.err,
errInfo: 'Unkown request'
);
}

View File

@ -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<Uint8List> _requestStreamController = StreamController()..close();
//Construct a payload stream which forward the incoming byte into temp file
StreamController<Uint8List> _payloadStreamController = StreamController()..close();
//Provide a request stream for caller functions to listen on
final StreamController<TCPRequest> _streamController = StreamController();
Stream<TCPRequest> 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();
}
}
}
}
}
}

View File

@ -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<String, Object?> _data;
UserIdentity({
required String userName,
required String userPasswdEncoded,
String? userPasswdEncodedNew
}): _data = {
"username": userName,
"passwd": userPasswdEncoded,
"newPasswd": userPasswdEncodedNew
};
UserIdentity.fromJSONObject(Map<String, Object?> 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<String, Object?> get jsonObject => _data;
}

View File

@ -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<String, Object?> _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<String, Object?> 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<String, Object?> get jsonObject => _data;
}

View File

@ -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<String, Object?> _data;
UserInfo({
required int userID,
required String userName,
String? userAvatar
}): _data = {
"userid": userID,
"username": userName,
"avatar": userAvatar
};
UserInfo.fromJSONObject(Map<String, Object?> 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<String, Object?> get jsonObject => _data;
}

View File

@ -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<String, Object?> _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<String, Object?> get body => _data['body'] as Map<String, Object?>? ?? {};
}

View File

@ -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<String, Object?>? 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<Uint8List> 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();
}
}
}

View File

@ -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;
}

369
pubspec.lock Normal file
View File

@ -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"

21
pubspec.yaml Normal file
View File

@ -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