├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── analysis_options.yaml ├── bin └── tcp_server.dart ├── lib ├── database.dart ├── requesthandler.dart ├── tcpcontroller │ ├── controller.dart │ ├── payload │ │ ├── identity.dart │ │ ├── message.dart │ │ └── userinfo.dart │ ├── request.dart │ └── response.dart └── utils │ └── typeconverter.dart ├── license ├── pubspec.lock └── pubspec.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | Dockerfile 3 | build/ 4 | .dart_tool/ 5 | .git/ 6 | .github/ 7 | .gitignore 8 | .packages 9 | 10 | .data/ 11 | .tmp/ 12 | 13 | # Conventional directory for build output. 14 | build/ 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | 5 | .tmp/ 6 | .data/ 7 | .data/** 8 | 9 | # Conventional directory for build output. 10 | build/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Specify the Dart SDK base image version using dart: (ex: dart:2.12) 2 | FROM dart:stable AS compile 3 | 4 | # Resolve app dependencies. 5 | WORKDIR /lchatserver 6 | COPY pubspec.* ./ 7 | RUN dart pub get 8 | 9 | # Copy app source code and AOT compile it. 10 | COPY . . 11 | # Ensure packages are still up-to-date if anything has changed 12 | RUN dart pub get --offline 13 | RUN dart compile exe bin/tcp_server.dart -o bin/tcp_server 14 | 15 | FROM ubuntu:latest 16 | 17 | RUN apt-get update && apt-get -y install libsqlite3-0 libsqlite3-dev 18 | 19 | # Copy the previously built executable into the scratch layer 20 | RUN mkdir /lchatserver 21 | COPY --from=compile /runtime/ /lchatserver/ 22 | COPY --from=compile /lchatserver/bin/tcp_server /lchatserver/bin/ 23 | 24 | # Start server. 25 | EXPOSE 20706 26 | WORKDIR /lchatserver/bin 27 | CMD ["/lchatserver/bin/tcp_server"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple TCP Server 2 | 3 | This is ~~a project of my Computer Internet course~~ a project inspired by my school homework. 4 | 5 | Several days before I'm asked to write a project on TCP server-client communication, and I wondered why not build a simple chat service beyond that which would be much more fun? 6 | 7 | Therefore here it is, a simple tcp server with several functions which makes it acts like a small chat app. 8 | 9 | ## Functions 10 | 11 | - User register / login / logout 12 | - Remember login state for device (simple token approach though) 13 | - Send messages via users 14 | - Search users and add contacts (and accept them of course) 15 | - Message sync via different devices 16 | - Send message to offline server (a SMTP-like approach) 17 | - File handling (transfer to and fetch from server) 18 | 19 | ## Notice 20 | 21 | - To support multilanguage, use `base64Encode(utf8.encode(yourMessageHere))` before wrapping client messages in json object and sending that to the server (the serve will crash!) 22 | - Always open a new TCP connection to fetch or send file 23 | 24 | ## Compile and Deploy 25 | 26 | To clone and run this project in command prompt, do the following under Windows environment with dart SDK configured. 27 | 28 | ```bash 29 | git clone https://github.com/Linloir/Simple-TCP-Server.git 30 | cd Simple-TCP-Server 31 | 32 | # [+] In case you want to build an exe 33 | # mkdir build 34 | # dart compile exe ./bin/tcp_server.dart -o ./build/tcp_server.exe 35 | # cd build 36 | # tcp_server.exe [listen port] 37 | 38 | dart run 39 | ``` 40 | 41 | ## Application Layer Protocol 42 | 43 | Since I was not allowed to base my homework on an existing HTTP protocol, I create my private protocol for this project. 44 | 45 | To communicate with the server, your data should conform the following formats: 46 | 47 | ### Overall Request / Response Structure 48 | 49 | Every request sent by any client to this server should and should only consist four regions: 50 | 51 | - Request length region (4 bytes) 52 | - Payload length region (4 bytes) 53 | - Request JSON (variable length) 54 | - Payload byte stream (variable length) 55 | 56 | Each section should interconnect with each other with no byte insets. 57 | 58 | ### Request JSON Region 59 | 60 | The request JSON region is self-explanatory, which contains a encoded JSON string describing the request from the client. 61 | 62 | #### Fields of Request JSON 63 | 64 | To get use of the JSON object, all you need is the table below describing the supported fields in a JSON object and the possible combinations between them: 65 | 66 | | Field | Content | Meaning | 67 | |:-----:|:-------:|:-------:| 68 | | `request` | | The type of the request | 69 | | `body` | *JSON object* | The information needed for the request | 70 | | `tokenid` | *number \| null* | The identifier of a device | 71 | 72 | #### Body of Request JSON 73 | 74 | The body field of the JSON object is the key part of a request; it contains the key information the server needs to perform the request command. 75 | 76 | There are mainly four different types of a body: 77 | 78 | The **UserInfo** body is used to describe the information of an arbitrary user: 79 | 80 | - `userid`: The user ID 81 | - `username`: The username 82 | - `avatar`: The base64 encoded form of the user's avatar 83 | 84 | The **UserIdentity** body is used as a credential of a specific user: 85 | 86 | - `username`: The username 87 | - `passwd`: The password of the user 88 | - `newPasswd`: The modified password *(Should only exists if a user is trying to modify his/hers password)* 89 | 90 | The **Message** body is used to describe a message: 91 | 92 | - `userid`: The user ID of the sender 93 | - `targetid`: The user ID of the reciever 94 | - `contenttype`: The type of content attached to the message, should only contain values among `plaintext`, `image` and `file` 95 | - `content`: The base64encoded utf8encoded string of the original message *(Should contain filename if the content type is 'file')* 96 | - `md5encoded`: The calculated md5 value of the encoded content string 97 | - `filemd5`: The attached file's md5 value *(Calculated at client side before sending the request; should only exist if the content type is 'file')* 98 | 99 | The **MessageIdentifier** is the identifier for a client to fetch a file for a message, it contains only the necessary part to identify a message: 100 | 101 | - `msgmd5`: The md5 of the message 102 | 103 | The **UserName** or **UserID** is self-explanatory, which contains only the username of a user in order to search the full info of the user: 104 | 105 | - `username`: The provided username **Or** `userid`: The provided user ID 106 | 107 | The usage of different body parts for different request types is described below: 108 | 109 | | Request Type | Body Part Contents | 110 | |:------------:|:------------------:| 111 | |`STATE`| *NONE* | 112 | |`REGISTER`| **UserIdentity** | 113 | |`LOGIN`| **UserIdentity** | 114 | |`LOGOUT`| *NONE* | 115 | |`SENDMSG`| **Message** | 116 | |`FETCHMSG`| *NONE* | 117 | |`FETCHFILE`| **MessageIdentifier** | 118 | |`SEARCHUSR`| **UserName** | 119 | |`ADDCONTACT`| **UserID** | 120 | |`FETCHCONTACT`| *NONE* | 121 | 122 | ### Response JSON Region 123 | 124 | #### Fields of Response JSON 125 | 126 | The fields of a response JSON is similar to that of a request JSON, excludes for the `tokenid` field. 127 | 128 | The response JSON also offers extra fields to describe the state of a performed command: 129 | 130 | | Field | Content | Meaning | 131 | |:-----:|:-------:|:-------:| 132 | |`status`|| The completion status of the request | 133 | |`info`| *String \| null* | Description of error *(Only exists if the status is 'ERR')* | 134 | 135 | #### Body of Response JSON 136 | 137 | The body of a response JSON object contains all possible types of a request JSON object, with the addition of two special types below. 138 | 139 | The **TokenInfo** body is self-explanatory, which carries the info of a token. This kind of response body only appears in an extra response preceding the original responsse of the first request from a new client device, which offers the client device a token number: 140 | 141 | - `tokenid`: The allocated token ID 142 | 143 | The **MessageList** body is also self-explanatory, which is a list of **Message** objects, this kind of response body exists in a `FETCHMSG` response: 144 | 145 | - `messages`: A list of **Message** objects 146 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /bin/tcp_server.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-06 15:44:16 4 | * @LastEditTime : 2022-10-23 10:33:58 5 | * @Description : 6 | */ 7 | 8 | import 'dart:io'; 9 | 10 | import 'package:tcp_server/database.dart'; 11 | import 'package:tcp_server/requesthandler.dart'; 12 | import 'package:tcp_server/tcpcontroller/controller.dart'; 13 | import 'package:tcp_server/tcpcontroller/payload/message.dart'; 14 | import 'package:tcp_server/tcpcontroller/request.dart'; 15 | import 'package:tcp_server/tcpcontroller/response.dart'; 16 | 17 | void main(List arguments) async { 18 | //Set address 19 | var port = arguments.isEmpty ? 20706 : int.tryParse(arguments[0]) ?? 20706; 20 | 21 | print('[L] [STARTUP ]-----------------------'); 22 | print('[L] Running at directory ${Directory.current.path}'); 23 | 24 | //Create nessesary working directories 25 | await Directory('${Directory.current.path}/.data').create(); 26 | await Directory('${Directory.current.path}/.data/.tmp').create(); 27 | await Directory('${Directory.current.path}/.data/files').create(); 28 | 29 | await DataBaseHelper().initialize(); 30 | Map> tokenMap = {}; 31 | Map> controllerMap = {}; 32 | var listenSocket = await ServerSocket.bind(InternetAddress.anyIPv4, port); 33 | listenSocket.listen( 34 | (socket) { 35 | var controller = TCPController(socket: socket); 36 | controller.responseStreamBroadcast.listen( 37 | null, 38 | onError: (_) { 39 | print('[L] [EXCEPTION]-----------------------'); 40 | print('[L] TCP Controller ran into exception'); 41 | print('[L] socket: ${controller.socket.address}:${controller.socket.port}'); 42 | var token = controllerMap[controller]; 43 | controllerMap.remove(controller); 44 | tokenMap[token]?.remove(controller); 45 | }, 46 | onDone: () { 47 | var token = controllerMap[controller]; 48 | controllerMap.remove(controller); 49 | tokenMap[token]?.remove(controller); 50 | }, 51 | cancelOnError: true 52 | ); 53 | controller.requestStreamBroadcast.listen( 54 | (request) async { 55 | print('[L] [INCOMING ]-----------------------'); 56 | print('[L] Incoming from ${controller.socket.remoteAddress}:${controller.socket.remotePort}'); 57 | if(request.requestType == RequestType.sendMessage) { 58 | print('[L] Message: (Message body)'); 59 | } 60 | else if(request.requestType == RequestType.modifyProfile) { 61 | print('[L] Profile: (Profile body)'); 62 | } 63 | else { 64 | print('[L] Message: ${request.toJSON}'); 65 | } 66 | if(!(await DataBaseHelper().isTokenValid(tokenid: request.tokenID))) { 67 | if(controllerMap[controller] == null) { 68 | controllerMap[controller] = (() async => (await DataBaseHelper().createToken()))(); 69 | } 70 | request.tokenID = await controllerMap[controller]; 71 | var tokenResponse = TCPResponse( 72 | type: ResponseType.token, 73 | status: ResponseStatus.ok, 74 | body: { 75 | "tokenid": request.tokenID 76 | } 77 | ); 78 | controller.outStream.add(tokenResponse); 79 | } 80 | tokenMap[request.tokenID!] ??= []; 81 | if(!tokenMap[request.tokenID]!.contains(controller)) { 82 | tokenMap[request.tokenID]!.add(controller); 83 | } 84 | switch(request.requestType) { 85 | case RequestType.checkState: { 86 | var response = await onCheckState(request, socket); 87 | try { 88 | controller.outStream.add(response); 89 | } catch (e) { 90 | print('[E] [EXCEPTION]-----------------------'); 91 | var token = controllerMap[controller]; 92 | controllerMap.remove(controller); 93 | tokenMap[token]?.remove(controller); 94 | } 95 | break; 96 | } 97 | case RequestType.register: { 98 | var response = await onRegister(request, socket); 99 | try { 100 | controller.outStream.add(response); 101 | } catch (e) { 102 | print('[E] [EXCEPTION]-----------------------'); 103 | var token = controllerMap[controller]; 104 | controllerMap.remove(controller); 105 | tokenMap[token]?.remove(controller); 106 | } 107 | break; 108 | } 109 | case RequestType.login: { 110 | var response = await onLogin(request, socket); 111 | try { 112 | controller.outStream.add(response); 113 | } catch (e) { 114 | print('[E] [EXCEPTION]-----------------------'); 115 | var token = controllerMap[controller]; 116 | controllerMap.remove(controller); 117 | tokenMap[token]?.remove(controller); 118 | } 119 | break; 120 | } 121 | case RequestType.logout: { 122 | var response = await onLogout(request, socket); 123 | controller.outStream.add(response); 124 | break; 125 | } 126 | case RequestType.profile: { 127 | var response = await onFetchProfile(request, socket); 128 | try { 129 | controller.outStream.add(response); 130 | } catch (e) { 131 | print('[E] [EXCEPTION]-----------------------'); 132 | var token = controllerMap[controller]; 133 | controllerMap.remove(controller); 134 | tokenMap[token]?.remove(controller); 135 | } 136 | break; 137 | } 138 | case RequestType.modifyProfile: { 139 | var response = await onModifyProfile(request, socket); 140 | try { 141 | controller.outStream.add(response); 142 | } catch (e) { 143 | print('[E] [EXCEPTION]-----------------------'); 144 | var token = controllerMap[controller]; 145 | controllerMap.remove(controller); 146 | tokenMap[token]?.remove(controller); 147 | } 148 | break; 149 | } 150 | case RequestType.modifyPassword: { 151 | var response = await onModifyPassword(request, socket); 152 | try { 153 | controller.outStream.add(response); 154 | } catch (e) { 155 | print('[E] [EXCEPTION]-----------------------'); 156 | var token = controllerMap[controller]; 157 | controllerMap.remove(controller); 158 | tokenMap[token]?.remove(controller); 159 | } 160 | break; 161 | } 162 | case RequestType.sendMessage: { 163 | //Forword Message 164 | var message = Message.fromJSONObject(request.body); 165 | await DataBaseHelper().setFetchHistoryFor( 166 | tokenID: request.tokenID, 167 | newTimeStamp: message.timestamp 168 | ); 169 | var originUserID = message.senderID; 170 | var onlineDevices = await DataBaseHelper().fetchTokenIDsViaUserID(userID: originUserID); 171 | for(var device in onlineDevices) { 172 | if(device == request.tokenID) { 173 | continue; 174 | } 175 | var targetControllers = tokenMap[device] ?? []; 176 | var forwardResponse = TCPResponse( 177 | type: ResponseType.forwardMessage, 178 | status: ResponseStatus.ok, 179 | body: message.jsonObject 180 | ); 181 | for(var controller in targetControllers) { 182 | try { 183 | print('[L] [MSGFOWARD]-----------------------'); 184 | print('[L] Forwarding message to ${controller.socket.remoteAddress}:${controller.socket.remotePort}'); 185 | controller.outStream.add(forwardResponse); 186 | } catch(e) { 187 | print('[E] [EXCEPTION]-----------------------'); 188 | var token = controllerMap[controller]; 189 | controllerMap.remove(controller); 190 | tokenMap[token]?.remove(controller); 191 | continue; 192 | } 193 | } 194 | // //Update Fetch Histories 195 | // await DataBaseHelper().setFetchHistoryFor( 196 | // tokenID: device, 197 | // newTimeStamp: message.timestamp 198 | // ); 199 | } 200 | var targetUserID = message.receiverID; 201 | var targetDevices = await DataBaseHelper().fetchTokenIDsViaUserID(userID: targetUserID); 202 | for(var device in targetDevices) { 203 | //Forward to socket 204 | var targetControllers = tokenMap[device] ?? []; 205 | var forwardResponse = TCPResponse( 206 | type: ResponseType.forwardMessage, 207 | status: ResponseStatus.ok, 208 | body: message.jsonObject 209 | ); 210 | for(int i = targetControllers.length - 1; i >= 0; i--) { 211 | var controller = targetControllers[i]; 212 | try{ 213 | print('[L] [MSGFOWARD]-----------------------'); 214 | print('[L] Forwarding message to ${controller.socket.remoteAddress}:${controller.socket.remotePort}'); 215 | controller.outStream.add(forwardResponse); 216 | } catch(e) { 217 | print('[E] [EXCEPTION]-----------------------'); 218 | var token = controllerMap[controller]; 219 | controllerMap.remove(controller); 220 | tokenMap[token]?.remove(controller); 221 | continue; 222 | } 223 | } 224 | // //Update Fetch Histories 225 | // await DataBaseHelper().setFetchHistoryFor( 226 | // tokenID: device, 227 | // newTimeStamp: message.timestamp 228 | // ); 229 | } 230 | var response = await onSendMessage(request, socket); 231 | try { 232 | controller.outStream.add(response); 233 | } catch (e) { 234 | print('[E] [EXCEPTION]-----------------------'); 235 | var token = controllerMap[controller]; 236 | controllerMap.remove(controller); 237 | tokenMap[token]?.remove(controller); 238 | } 239 | break; 240 | } 241 | case RequestType.fetchMessage: { 242 | var response = await onFetchMessage(request, socket); 243 | try { 244 | controller.outStream.add(response); 245 | } catch (e) { 246 | print('[E] [EXCEPTION]-----------------------'); 247 | var token = controllerMap[controller]; 248 | controllerMap.remove(controller); 249 | tokenMap[token]?.remove(controller); 250 | } 251 | break; 252 | } 253 | case RequestType.findFile: { 254 | var response = await onFindFile(request, socket); 255 | try { 256 | controller.outStream.add(response); 257 | } catch (e) { 258 | print('[E] [EXCEPTION]-----------------------'); 259 | var token = controllerMap[controller]; 260 | controllerMap.remove(controller); 261 | tokenMap[token]?.remove(controller); 262 | } 263 | break; 264 | } 265 | case RequestType.fetchFile: { 266 | var response = await onFetchFile(request, socket); 267 | try { 268 | controller.outStream.add(response); 269 | } catch (e) { 270 | print('[E] [EXCEPTION]-----------------------'); 271 | var token = controllerMap[controller]; 272 | controllerMap.remove(controller); 273 | tokenMap[token]?.remove(controller); 274 | } 275 | break; 276 | } 277 | case RequestType.searchUser: { 278 | var response = await onSearchUser(request, socket); 279 | try { 280 | controller.outStream.add(response); 281 | } catch (e) { 282 | print('[E] [EXCEPTION]-----------------------'); 283 | var token = controllerMap[controller]; 284 | controllerMap.remove(controller); 285 | tokenMap[token]?.remove(controller); 286 | } 287 | break; 288 | } 289 | case RequestType.addContact: { 290 | var response = await onAddContact(request, socket); 291 | try { 292 | controller.outStream.add(response); 293 | } catch (e) { 294 | print('[E] [EXCEPTION]-----------------------'); 295 | var token = controllerMap[controller]; 296 | controllerMap.remove(controller); 297 | tokenMap[token]?.remove(controller); 298 | } 299 | var contactResponse = await onFetchContact( 300 | TCPRequest.fromData( 301 | type: RequestType.fetchContact, 302 | body: {}, 303 | tokenID: request.tokenID 304 | ), 305 | socket 306 | ); 307 | controller.outStream.add(contactResponse); 308 | break; 309 | } 310 | case RequestType.fetchContact: { 311 | var response = await onFetchContact(request, socket); 312 | try { 313 | controller.outStream.add(response); 314 | } catch (e) { 315 | print('[E] [EXCEPTION]-----------------------'); 316 | var token = controllerMap[controller]; 317 | controllerMap.remove(controller); 318 | tokenMap[token]?.remove(controller); 319 | } 320 | break; 321 | } 322 | case RequestType.ackFetch: { 323 | onAckFetch(request, socket); 324 | break; 325 | } 326 | case RequestType.unknown: { 327 | var response = await onUnknownRequest(request, socket); 328 | try { 329 | controller.outStream.add(response); 330 | } catch (e) { 331 | print('[E] [EXCEPTION]-----------------------'); 332 | var token = controllerMap[controller]; 333 | controllerMap.remove(controller); 334 | tokenMap[token]?.remove(controller); 335 | } 336 | break; 337 | } 338 | default: { 339 | print('[E] Drop out of switch case'); 340 | } 341 | } 342 | //Clear temp file 343 | if(request.payload?.existsSync() ?? false) { 344 | request.payload?.delete(); 345 | } 346 | }, 347 | onError: (e) { 348 | print(e); 349 | var token = controllerMap[controller]; 350 | controllerMap.remove(controller); 351 | tokenMap[token]?.remove(controller); 352 | }, 353 | onDone: () { 354 | var token = controllerMap[controller]; 355 | controllerMap.remove(controller); 356 | tokenMap[token]?.remove(controller); 357 | } 358 | ); 359 | }, 360 | cancelOnError: true 361 | ); 362 | } 363 | -------------------------------------------------------------------------------- /lib/database.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-06 16:15:01 4 | * @LastEditTime : 2022-10-22 21:08:27 5 | * @Description : 6 | */ 7 | 8 | import 'dart:io'; 9 | 10 | import 'package:sqflite_common/sqlite_api.dart'; 11 | import 'package:sqflite_common_ffi/sqflite_ffi.dart'; 12 | import 'package:tcp_server/tcpcontroller/payload/identity.dart'; 13 | import 'package:tcp_server/tcpcontroller/payload/message.dart'; 14 | import 'package:tcp_server/tcpcontroller/payload/userinfo.dart'; 15 | 16 | class DataBaseHelper { 17 | static final DataBaseHelper _helper = DataBaseHelper._internal(); 18 | late final Database _database; 19 | 20 | factory DataBaseHelper() { 21 | return _helper; 22 | } 23 | 24 | DataBaseHelper._internal(); 25 | 26 | Future initialize() async { 27 | _database = await databaseFactoryFfi.openDatabase( 28 | '${Directory.current.path}/.data/.tmp/database.db', 29 | options: OpenDatabaseOptions( 30 | version: 1, 31 | onCreate: (db, version) async { 32 | print('[L] Creating Database.'); 33 | await db.execute( 34 | ''' 35 | CREATE TABLE users ( 36 | userid integer primary key autoincrement, 37 | username text unique not null, 38 | passwd text not null, 39 | avatar text 40 | ); 41 | create table msgs ( 42 | userid integer, 43 | targetid integer, 44 | contenttype text not null, 45 | content text not null, 46 | timestamp integer, 47 | md5encoded text primary key not null 48 | ); 49 | create table contacts ( 50 | userid integer, 51 | targetid integer, 52 | primary key (userid, targetid) 53 | ); 54 | create table tokens ( 55 | tokenid integer primary key autoincrement, 56 | createtime integer not null, 57 | lastused integer not null 58 | ); 59 | create table bindings ( 60 | tokenid integer primary key, 61 | userid integer 62 | ); 63 | create table histories ( 64 | tokenid integer, 65 | userid integer, 66 | lastfetch integer not null, 67 | primary key (tokenid, userid) 68 | ); 69 | create table files ( 70 | filemd5 text primary key not null, 71 | dir text not null 72 | ); 73 | create table msgfiles ( 74 | msgmd5 text not null, 75 | filemd5 text not null, 76 | primary key (msgmd5, filemd5) 77 | ); 78 | ''' 79 | ); 80 | print('[L] Database created'); 81 | }, 82 | ) 83 | ); 84 | } 85 | 86 | Future isTokenValid({ 87 | required int? tokenid, 88 | }) async { 89 | if(tokenid == null) { 90 | return false; 91 | } 92 | 93 | var tokenQueryResult = await _database.query( 94 | 'tokens', 95 | where: 'tokenid = ?', 96 | whereArgs: [ 97 | tokenid 98 | ] 99 | ); 100 | 101 | return tokenQueryResult.isNotEmpty; 102 | } 103 | 104 | //Creates new token 105 | Future createToken() async { 106 | //Insert new row 107 | var row = await _database.rawInsert( 108 | ''' 109 | insert into tokens(createtime, lastused) 110 | values (?, ?) 111 | ''', 112 | [ 113 | DateTime.now().millisecondsSinceEpoch, 114 | DateTime.now().millisecondsSinceEpoch 115 | ] 116 | ); 117 | //Fetch new row 118 | var newToken = (await _database.query( 119 | 'tokens', 120 | where: 'rowid = $row', 121 | ))[0]['tokenid'] as int; 122 | //Return token 123 | return newToken; 124 | } 125 | 126 | Future checkLoginState({ 127 | required int? tokenID 128 | }) async { 129 | if(tokenID == null) { 130 | throw Exception('Invalid device token'); 131 | } 132 | 133 | var bindingQueryResult = await _database.query( 134 | 'bindings natural join users', 135 | where: 'tokenid = ?', 136 | whereArgs: [ 137 | tokenID 138 | ] 139 | ); 140 | if(bindingQueryResult.isNotEmpty) { 141 | return UserInfo( 142 | userID: bindingQueryResult[0]['userid'] as int, 143 | userName: bindingQueryResult[0]['username'] as String, 144 | userAvatar: bindingQueryResult[0]['avatar'] as String? 145 | ); 146 | } 147 | else { 148 | throw Exception('User not logged in'); 149 | } 150 | } 151 | 152 | Future logIn({ 153 | required UserIdentity identity, 154 | required int? tokenID 155 | }) async { 156 | if(tokenID == null) { 157 | throw Exception('Invalid device token'); 158 | } 159 | 160 | var userIdentities = await _database.query( 161 | 'users', 162 | where: 'username = ?', 163 | whereArgs: [ 164 | identity.userName 165 | ] 166 | ); 167 | if(userIdentities.isNotEmpty) { 168 | var user = userIdentities[0]; 169 | if(user['passwd'] == identity.userPasswd) { 170 | //Query for existed token binding 171 | var existBindings = await _database.query( 172 | 'bindings', 173 | where: 'tokenid = ?', 174 | whereArgs: [ 175 | tokenID 176 | ] 177 | ); 178 | if(existBindings.isEmpty) { 179 | //Add new binding 180 | await _database.insert( 181 | 'bindings', 182 | { 183 | 'tokenid': tokenID, 184 | 'userid': user['userid'] 185 | } 186 | ); 187 | } 188 | else { 189 | //Update token binding 190 | await _database.update( 191 | 'bindings', 192 | { 193 | 'tokenid': tokenID, 194 | 'userid': user['userid'] 195 | }, 196 | where: 'tokenid = ?', 197 | whereArgs: [ 198 | tokenID 199 | ] 200 | ); 201 | } 202 | return UserInfo( 203 | userID: user['userid'] as int, 204 | userName: user['username'] as String, 205 | userAvatar: user['avatar'] as String? 206 | ); 207 | } 208 | else { 209 | throw Exception('Invalid password'); 210 | } 211 | } 212 | else { 213 | throw Exception('User not found'); 214 | } 215 | } 216 | 217 | Future logOut({ 218 | required int? tokenID 219 | }) async { 220 | if(tokenID == null) { 221 | throw Exception('Invalid device token'); 222 | } 223 | 224 | //Delete binding 225 | await _database.delete( 226 | 'bindings', 227 | where: 'tokenid = ?', 228 | whereArgs: [ 229 | tokenID 230 | ] 231 | ); 232 | } 233 | 234 | Future registerUser({ 235 | required UserIdentity identity, 236 | required int? tokenID 237 | }) async { 238 | if(tokenID == null) { 239 | throw Exception('Invalid device token'); 240 | } 241 | 242 | //Insert into users 243 | try { 244 | await _database.transaction((txn) async { 245 | var result = await txn.query( 246 | 'users', 247 | where: 'username = ?', 248 | whereArgs: [ 249 | identity.userName 250 | ] 251 | ); 252 | if(result.isNotEmpty) { 253 | throw Exception('Username already exists'); 254 | } 255 | await txn.insert( 256 | 'users', 257 | { 258 | 'username': identity.userName, 259 | 'passwd': identity.userPasswd, 260 | 'avatar': null 261 | }, 262 | conflictAlgorithm: ConflictAlgorithm.rollback 263 | ); 264 | }); 265 | } catch (e) { 266 | rethrow; 267 | } 268 | 269 | //Get new userid 270 | var newUserID = (await _database.query( 271 | 'users', 272 | where: 'username = ?', 273 | whereArgs: [ 274 | identity.userName 275 | ] 276 | ))[0]['userid'] as int; 277 | 278 | //Insert into bindings 279 | await _database.insert( 280 | 'bindings', 281 | { 282 | 'tokenid': tokenID, 283 | 'userid': newUserID 284 | } 285 | ); 286 | 287 | return UserInfo( 288 | userID: newUserID, 289 | userName: identity.userName, 290 | userAvatar: null 291 | ); 292 | } 293 | 294 | Future modifyUserPassword({ 295 | required UserIdentity newIdentity, 296 | required int? tokenID 297 | }) async { 298 | if(tokenID == null) { 299 | throw Exception('Invalid device token'); 300 | } 301 | 302 | //Find current binded user 303 | var currentUserQueryResult = await _database.query( 304 | 'bindings natural join users', 305 | where: 'tokenid = ?', 306 | whereArgs: [ 307 | tokenID 308 | ] 309 | ); 310 | if(currentUserQueryResult.isEmpty) { 311 | throw Exception('User not logged in'); 312 | } 313 | var currentUser = currentUserQueryResult[0]; 314 | 315 | //Verify user identity 316 | if(currentUser['passwd'] as String != newIdentity.userPasswd) { 317 | throw Exception('Wrong password'); 318 | } 319 | else { 320 | try { 321 | //Modify database 322 | await _database.update( 323 | 'users', 324 | { 325 | 'passwd': newIdentity.userPasswdNew 326 | }, 327 | where: 'userid = ${currentUser['userid'] as int}', 328 | conflictAlgorithm: ConflictAlgorithm.rollback 329 | ); 330 | } catch (conflict) { 331 | throw Exception(['Database failure', conflict.toString()]); 332 | } 333 | } 334 | } 335 | 336 | //Returns a list of unfetched messages in JSON format 337 | Future> fetchMessagesFor({ 338 | required int? tokenID 339 | }) async { 340 | if(tokenID == null) { 341 | throw Exception('Invalid device token'); 342 | } 343 | 344 | //Find userID and last fetched time 345 | var userIdQueryResult = await _database.query( 346 | 'bindings natural left outer join histories', 347 | columns: ['userid', 'lastfetch'], 348 | where: 'tokenid = ?', 349 | whereArgs: [ 350 | tokenID 351 | ] 352 | ); 353 | if(userIdQueryResult.isEmpty) { 354 | throw Exception('User not logged in'); 355 | } 356 | var userID = userIdQueryResult[0]['userid'] as int; 357 | var lastFetch = userIdQueryResult[0]['lastfetch'] as int?; 358 | if(lastFetch == null) { 359 | //First fetch, add to fetch history 360 | await _database.insert( 361 | 'histories', 362 | { 363 | 'tokenid': tokenID, 364 | 'userid': userID, 365 | 'lastfetch': 0 366 | } 367 | ); 368 | lastFetch = 0; 369 | } 370 | 371 | //Fetch unfetched messages 372 | var unfetchMsgQueryResult = await _database.query( 373 | 'msgs left outer join msgfiles on msgs.md5encoded = msgfiles.msgmd5', 374 | columns: [ 375 | 'msgs.userid as userid', 376 | 'msgs.targetid as targetid', 377 | 'msgs.contenttype as contenttype', 378 | 'msgs.content as content', 379 | 'msgs.timestamp as timestamp', 380 | 'msgs.md5encoded as md5encoded', 381 | 'msgfiles.filemd5 as filemd5' 382 | ], 383 | where: '(userid = ? or targetid = ?) and timestamp > ?', 384 | whereArgs: [ 385 | userID, 386 | userID, 387 | lastFetch 388 | ], 389 | orderBy: 'timestamp desc' 390 | ); 391 | var unfetchMessages = unfetchMsgQueryResult.map((message) { 392 | return Message( 393 | userid: message['userid'] as int, 394 | targetid: message['targetid'] as int, 395 | contenttype: MessageType.fromStringLiteral( 396 | message['contenttype'] as String 397 | ), 398 | content: message['content'] as String, 399 | timestamp: message['timestamp'] as int, 400 | md5encoded: message['md5encoded'] as String, 401 | filemd5: message['filemd5'] as String? 402 | ); 403 | }).toList(); 404 | 405 | //Set new fetch history 406 | // if(unfetchMsgQueryResult.isNotEmpty) { 407 | // await _database.update( 408 | // 'histories', 409 | // { 410 | // 'lastfetch': unfetchMsgQueryResult[0]['timestamp'] 411 | // }, 412 | // where: 'tokenid = ? and userid = ?', 413 | // whereArgs: [ 414 | // tokenID, 415 | // userID 416 | // ] 417 | // ); 418 | // } 419 | 420 | //return result 421 | return unfetchMessages; 422 | } 423 | 424 | Future setFetchHistoryFor({ 425 | required int? tokenID, 426 | required int newTimeStamp 427 | }) async { 428 | if(tokenID == null) { 429 | throw Exception('Invalid device token'); 430 | } 431 | 432 | //Get current userid 433 | var bindingQueryResult = await _database.query( 434 | 'bindings natural left outer join histories', 435 | where: 'bindings.tokenid = ?', 436 | whereArgs: [ 437 | tokenID 438 | ] 439 | ); 440 | if(bindingQueryResult.isEmpty) { 441 | //Be silence on err 442 | return; 443 | } 444 | var userID = bindingQueryResult[0]['userid'] as int; 445 | 446 | //Check for fetch history 447 | var lastFetch = bindingQueryResult[0]['lastfetch'] as int?; 448 | if(lastFetch == null) { 449 | //First fetch, add to fetch history 450 | await _database.insert( 451 | 'histories', 452 | { 453 | 'tokenid': tokenID, 454 | 'userid': userID, 455 | 'lastfetch': newTimeStamp 456 | } 457 | ); 458 | } 459 | else { 460 | //Update fetch history 461 | await _database.update( 462 | 'histories', 463 | { 464 | 'lastfetch': newTimeStamp 465 | }, 466 | where: 'tokenid = ? and userid = ?', 467 | whereArgs: [ 468 | tokenID, 469 | userID 470 | ] 471 | ); 472 | } 473 | } 474 | 475 | Future storeMessage({ 476 | required Message msg, 477 | String? fileMd5 478 | }) async { 479 | try { 480 | await _database.insert( 481 | 'msgs', 482 | { 483 | 'userid': msg.senderID, 484 | 'targetid': msg.receiverID, 485 | 'contenttype': msg.contentType.literal, 486 | 'content': msg.content, 487 | 'timestamp': msg.timestamp, 488 | 'md5encoded': msg.md5encoded, 489 | } 490 | ); 491 | } catch (err) { 492 | print('[E] Database failure on message storage:'); 493 | print('[>] $err'); 494 | } 495 | if(msg.contentType == MessageType.file) { 496 | if(fileMd5 == null) { 497 | await _database.delete( 498 | 'msgs', 499 | where: 'md5encoded = ?', 500 | whereArgs: [ 501 | msg.md5encoded 502 | ] 503 | ); 504 | throw Exception('Missing file for message'); 505 | } 506 | await _database.insert( 507 | 'msgfiles', 508 | { 509 | 'msgmd5': msg.md5encoded, 510 | 'filemd5': fileMd5 511 | }, 512 | conflictAlgorithm: ConflictAlgorithm.replace 513 | ); 514 | } 515 | } 516 | 517 | Future storeFile({ 518 | required File? tempFile, 519 | required String? fileMd5 520 | }) async { 521 | if(tempFile == null || fileMd5 == null) { 522 | throw Exception('Missing file parts'); 523 | } 524 | var filePath = '${Directory.current.path}/.data/files/$fileMd5'; 525 | await tempFile.copy(filePath); 526 | try { 527 | var sameFile = await _database.query( 528 | 'files', 529 | where: 'filemd5 = ?', 530 | whereArgs: [ 531 | fileMd5 532 | ] 533 | ); 534 | if(sameFile.isNotEmpty) { 535 | return; 536 | } 537 | await _database.insert( 538 | 'files', 539 | { 540 | 'filemd5': fileMd5, 541 | 'dir': filePath 542 | }, 543 | conflictAlgorithm: ConflictAlgorithm.rollback 544 | ); 545 | } catch (conflict) { 546 | throw Exception(['Database failure', conflict.toString()]); 547 | } 548 | } 549 | 550 | Future findFile({ 551 | required String fileMd5 552 | }) async { 553 | var targetFile = await _database.query( 554 | 'files', 555 | where: 'filemd5 = ?', 556 | whereArgs: [ 557 | fileMd5 558 | ] 559 | ); 560 | return targetFile.isNotEmpty; 561 | } 562 | 563 | Future fetchFilePath({ 564 | required String msgMd5 565 | }) async { 566 | var queryResult = await _database.query( 567 | 'msgfiles natural join files', 568 | where: 'msgfiles.msgmd5 = ?', 569 | whereArgs: [ 570 | msgMd5 571 | ] 572 | ); 573 | if(queryResult.isEmpty) { 574 | throw Exception('File not found'); 575 | } 576 | return queryResult[0]['dir'] as String; 577 | } 578 | 579 | Future fetchUserInfoViaID({ 580 | required int userid 581 | }) async { 582 | 583 | //Find current binded userID 584 | var userQueryResult = (await _database.query( 585 | 'users', 586 | where: 'userid = ?', 587 | whereArgs: [ 588 | userid 589 | ] 590 | )); 591 | 592 | if(userQueryResult.isEmpty) { 593 | throw Exception('User not found'); 594 | } 595 | 596 | return UserInfo( 597 | userID: userQueryResult[0]['userid'] as int, 598 | userName: userQueryResult[0]['username'] as String, 599 | userAvatar: userQueryResult[0]['avatar'] as String? 600 | ); 601 | } 602 | 603 | Future modifyUserInfo({ 604 | required UserInfo userInfo, 605 | required int? tokenID 606 | }) async { 607 | if(tokenID == null) { 608 | throw Exception('Invalid device token'); 609 | } 610 | 611 | //Find current binded userID 612 | var currentUserIDQueryResult = (await _database.query( 613 | 'bindings', 614 | where: 'tokenid = ?', 615 | whereArgs: [ 616 | tokenID 617 | ] 618 | )); 619 | if(currentUserIDQueryResult.isEmpty) { 620 | throw Exception('User not logged in'); 621 | } 622 | var currentUserID = currentUserIDQueryResult[0]['userid'] as int; 623 | 624 | //Update database 625 | try { 626 | await _database.update( 627 | 'users', 628 | { 629 | 'username': userInfo.userName, 630 | 'avatar': userInfo.userAvatar 631 | }, 632 | where: 'userid = ?', 633 | whereArgs: [ 634 | currentUserID 635 | ], 636 | conflictAlgorithm: ConflictAlgorithm.rollback 637 | ); 638 | } catch (conflict) { 639 | throw Exception(['Database failure', conflict.toString()]); 640 | } 641 | 642 | //Return result 643 | return UserInfo( 644 | userID: currentUserID, 645 | userName: userInfo.userName, 646 | userAvatar: userInfo.userAvatar 647 | ); 648 | } 649 | 650 | Future fetchUserInfoViaUsername({ 651 | required String username 652 | }) async { 653 | var targetUserQueryResult = await _database.query( 654 | 'users', 655 | columns: [ 656 | 'userid', 657 | 'username', 658 | 'avatar' 659 | ], 660 | where: 'username = ?', 661 | whereArgs: [ 662 | username 663 | ] 664 | ); 665 | if(targetUserQueryResult.isNotEmpty) { 666 | return UserInfo( 667 | userID: targetUserQueryResult[0]['userid'] as int, 668 | userName: targetUserQueryResult[0]['username'] as String, 669 | userAvatar: targetUserQueryResult[0]['avatar'] as String? 670 | ); 671 | } 672 | else { 673 | throw Exception('User not found'); 674 | } 675 | } 676 | 677 | Future> fetchContact({ 678 | required int? tokenID 679 | }) async { 680 | if(tokenID == null) { 681 | throw Exception('Invalid device token'); 682 | } 683 | 684 | //Find current binded userID 685 | var currentUserIDQueryResult = (await _database.query( 686 | 'bindings', 687 | where: 'tokenid = ?', 688 | whereArgs: [ 689 | tokenID 690 | ] 691 | )); 692 | 693 | if(currentUserIDQueryResult.isEmpty) { 694 | throw Exception('User not logged in'); 695 | } 696 | 697 | var currentUserID = currentUserIDQueryResult[0]['userid'] as int; 698 | 699 | //Fetch all contacts 700 | var contactsQueryResult = await _database.query( 701 | 'contacts as I join contacts as P on I.targetid = P.userid join users on I.targetid = users.userid', 702 | columns: ['I.targetid as userid', 'users.username as username', 'users.avatar as avatar'], 703 | where: 'I.userid = P.targetid and I.userid = ?', 704 | whereArgs: [ 705 | currentUserID 706 | ] 707 | ); 708 | 709 | //Convert to encodable objects 710 | var contactsEncodable = contactsQueryResult.map((contact) { 711 | return UserInfo( 712 | userID: contact['userid'] as int, 713 | userName: contact['username'] as String, 714 | userAvatar: contact['avatar'] as String? 715 | ); 716 | }).toList(); 717 | 718 | return contactsEncodable; 719 | } 720 | 721 | Future> fetchPendingContacts({ 722 | required int? tokenID 723 | }) async { 724 | if(tokenID == null) { 725 | throw Exception('Invalid device token'); 726 | } 727 | 728 | var currentUserIDQueryResult = (await _database.query( 729 | 'bindings', 730 | where: 'tokenid = ?', 731 | whereArgs: [ 732 | tokenID 733 | ] 734 | )); 735 | 736 | if(currentUserIDQueryResult.isEmpty) { 737 | throw Exception('User not logged in'); 738 | } 739 | 740 | var currentUserID = currentUserIDQueryResult[0]['userid'] as int; 741 | 742 | //Fetch pending contacts 743 | var contactsQueryResult = await _database.query( 744 | 'contacts join users on contacts.targetid = users.userid', 745 | columns: ['contacts.targetid as userid', 'users.username as username', 'users.avatar as avatar'], 746 | where: '''contacts.userid = ? and not exists ( 747 | select * from contacts as S 748 | where contacts.targetid = S.userid and contacts.userid = S.targetid 749 | )''', 750 | whereArgs: [ 751 | currentUserID 752 | ] 753 | ); 754 | 755 | //Convert to encodable objects 756 | var contactsEncodable = contactsQueryResult.map((contact) { 757 | return UserInfo( 758 | userID: contact['userid'] as int, 759 | userName: contact['username'] as String, 760 | userAvatar: contact['avatar'] as String? 761 | ); 762 | }).toList(); 763 | 764 | return contactsEncodable; 765 | } 766 | 767 | Future> fetchRequestingContacts({ 768 | required int? tokenID 769 | }) async { 770 | if(tokenID == null) { 771 | throw Exception('Invalid device token'); 772 | } 773 | 774 | //Find current binded userID 775 | var currentUserIDQueryResult = (await _database.query( 776 | 'bindings', 777 | where: 'tokenid = ?', 778 | whereArgs: [ 779 | tokenID 780 | ] 781 | )); 782 | 783 | if(currentUserIDQueryResult.isEmpty) { 784 | throw Exception('User not logged in'); 785 | } 786 | 787 | var currentUserID = currentUserIDQueryResult[0]['userid'] as int; 788 | 789 | //Fetch pending contacts 790 | var contactsQueryResult = await _database.query( 791 | 'contacts join users on contacts.userid = users.userid', 792 | columns: ['contacts.userid as userid', 'users.username as username', 'users.avatar as avatar'], 793 | where: '''contacts.targetid = ? and not exists ( 794 | select * from contacts as S 795 | where contacts.targetid = S.userid and contacts.userid = S.targetid 796 | )''', 797 | whereArgs: [ 798 | currentUserID 799 | ] 800 | ); 801 | 802 | //Convert to encodable objects 803 | var contactsEncodable = contactsQueryResult.map((contact) { 804 | return UserInfo( 805 | userID: contact['userid'] as int, 806 | userName: contact['username'] as String, 807 | userAvatar: contact['avatar'] as String? 808 | ); 809 | }).toList(); 810 | 811 | return contactsEncodable; 812 | } 813 | 814 | Future addContact({ 815 | required int? tokenID, 816 | required int userID 817 | }) async { 818 | if(tokenID == null) { 819 | throw Exception('Invalid device token'); 820 | } 821 | 822 | //Find current binded userID 823 | var currentUserID = (await _database.query( 824 | 'bindings', 825 | where: 'tokenid = ?', 826 | whereArgs: [ 827 | tokenID 828 | ] 829 | ))[0]['userid'] as int; 830 | 831 | //Add contacts 832 | await _database.insert( 833 | 'contacts', 834 | { 835 | 'userid': currentUserID, 836 | 'targetid': userID 837 | }, 838 | conflictAlgorithm: ConflictAlgorithm.ignore 839 | ); 840 | } 841 | 842 | Future> fetchTokenIDsViaUserID({ 843 | required int userID 844 | }) async { 845 | var tokenIDQueryResult = await _database.query( 846 | 'bindings', 847 | where: 'userid = ?', 848 | whereArgs: [ 849 | userID 850 | ] 851 | ); 852 | 853 | return tokenIDQueryResult.map((token) { 854 | return token['tokenid'] as int; 855 | }).toList(); 856 | } 857 | } 858 | -------------------------------------------------------------------------------- /lib/requesthandler.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-08 20:52:48 4 | * @LastEditTime : 2022-10-22 20:56:15 5 | * @Description : 6 | */ 7 | 8 | import 'dart:io'; 9 | 10 | import 'package:tcp_server/database.dart'; 11 | import 'package:tcp_server/tcpcontroller/payload/identity.dart'; 12 | import 'package:tcp_server/tcpcontroller/payload/message.dart'; 13 | import 'package:tcp_server/tcpcontroller/payload/userinfo.dart'; 14 | import 'package:tcp_server/tcpcontroller/request.dart'; 15 | import 'package:tcp_server/tcpcontroller/response.dart'; 16 | 17 | Future onCheckState(TCPRequest request, Socket socket) async { 18 | try { 19 | var userInfo = await DataBaseHelper().checkLoginState(tokenID: request.tokenID); 20 | return TCPResponse( 21 | type: ResponseType.fromRequestType(request.requestType), 22 | status: ResponseStatus.ok, 23 | body: userInfo.jsonObject 24 | ); 25 | } on Exception catch (exception) { 26 | return TCPResponse( 27 | type: ResponseType.fromRequestType(request.requestType), 28 | status: ResponseStatus.err, 29 | errInfo: exception.toString() 30 | ); 31 | } 32 | } 33 | 34 | Future onRegister(TCPRequest request, Socket socket) async { 35 | try { 36 | UserIdentity identity = UserIdentity.fromJSONObject(request.body); 37 | var newUserInfo = await DataBaseHelper().registerUser( 38 | identity: identity, 39 | tokenID: request.tokenID 40 | ); 41 | return TCPResponse( 42 | type: ResponseType.fromRequestType(request.requestType), 43 | status: ResponseStatus.ok, 44 | body: newUserInfo.jsonObject 45 | ); 46 | } on Exception catch (exception) { 47 | return TCPResponse( 48 | type: ResponseType.fromRequestType(request.requestType), 49 | status: ResponseStatus.err, 50 | errInfo: exception.toString() 51 | ); 52 | } 53 | } 54 | 55 | Future onLogin(TCPRequest request, Socket socket) async { 56 | try { 57 | var userInfo = await DataBaseHelper().logIn( 58 | identity: UserIdentity.fromJSONObject(request.body), 59 | tokenID: request.tokenID 60 | ); 61 | return TCPResponse( 62 | type: ResponseType.fromRequestType(request.requestType), 63 | status: ResponseStatus.ok, 64 | body: userInfo.jsonObject 65 | ); 66 | } on Exception catch (exception) { 67 | return TCPResponse( 68 | type: ResponseType.fromRequestType(request.requestType), 69 | status: ResponseStatus.err, 70 | errInfo: exception.toString() 71 | ); 72 | } 73 | } 74 | 75 | Future onLogout(TCPRequest request, Socket socket) async { 76 | try { 77 | await DataBaseHelper().logOut(tokenID: request.tokenID); 78 | return TCPResponse( 79 | type: ResponseType.fromRequestType(request.requestType), 80 | status: ResponseStatus.ok, 81 | ); 82 | } on Exception catch (exception) { 83 | return TCPResponse( 84 | type: ResponseType.fromRequestType(request.requestType), 85 | status: ResponseStatus.err, 86 | errInfo: exception.toString() 87 | ); 88 | } 89 | } 90 | 91 | Future onFetchProfile(TCPRequest request, Socket socket) async { 92 | try { 93 | var userInfo = await DataBaseHelper().fetchUserInfoViaID(userid: request.body['userid'] as int); 94 | return TCPResponse( 95 | type: ResponseType.fromRequestType(request.requestType), 96 | status: ResponseStatus.ok, 97 | body: userInfo.jsonObject 98 | ); 99 | } on Exception catch (exception) { 100 | return TCPResponse( 101 | type: ResponseType.fromRequestType(request.requestType), 102 | status: ResponseStatus.err, 103 | errInfo: exception.toString() 104 | ); 105 | } 106 | } 107 | 108 | Future onModifyPassword(TCPRequest request, Socket socket) async { 109 | try { 110 | await DataBaseHelper().modifyUserPassword( 111 | newIdentity: UserIdentity.fromJSONObject(request.body), 112 | tokenID: request.tokenID 113 | ); 114 | return TCPResponse( 115 | type: ResponseType.fromRequestType(request.requestType), 116 | status: ResponseStatus.ok 117 | ); 118 | } on Exception catch (exception) { 119 | return TCPResponse( 120 | type: ResponseType.fromRequestType(request.requestType), 121 | status: ResponseStatus.err, 122 | errInfo: exception.toString() 123 | ); 124 | } 125 | } 126 | 127 | Future onModifyProfile(TCPRequest request, Socket socket) async { 128 | try { 129 | var newUserInfo = await DataBaseHelper().modifyUserInfo( 130 | userInfo: UserInfo.fromJSONObject(request.body), 131 | tokenID: request.tokenID 132 | ); 133 | return TCPResponse( 134 | type: ResponseType.fromRequestType(request.requestType), 135 | status: ResponseStatus.ok, 136 | body: newUserInfo.jsonObject 137 | ); 138 | } on Exception catch (exception) { 139 | return TCPResponse( 140 | type: ResponseType.fromRequestType(request.requestType), 141 | status: ResponseStatus.err, 142 | errInfo: exception.toString() 143 | ); 144 | } 145 | } 146 | 147 | Future onSendMessage(TCPRequest request, Socket socket) async { 148 | try { 149 | var message = Message.fromJSONObject(request.body); 150 | if(message.contentType == MessageType.file) { 151 | await DataBaseHelper().storeFile( 152 | tempFile: request.payload, 153 | fileMd5: message.fileMd5 154 | ); 155 | } 156 | //Store message 157 | await DataBaseHelper().storeMessage( 158 | msg: message, 159 | fileMd5: message.fileMd5 160 | ); 161 | return TCPResponse( 162 | type: ResponseType.fromRequestType(request.requestType), 163 | status: ResponseStatus.ok, 164 | body: { 165 | 'md5encoded': message.md5encoded 166 | } 167 | ); 168 | } on Exception catch (exception) { 169 | return TCPResponse( 170 | type: ResponseType.fromRequestType(request.requestType), 171 | status: ResponseStatus.err, 172 | errInfo: exception.toString(), 173 | ); 174 | } 175 | } 176 | 177 | Future onFetchMessage(TCPRequest request, Socket socket) async { 178 | try { 179 | var messages = await DataBaseHelper().fetchMessagesFor(tokenID: request.tokenID); 180 | return TCPResponse( 181 | type: ResponseType.fromRequestType(request.requestType), 182 | status: ResponseStatus.ok, 183 | body: { 184 | 'messages': messages.map((e) => e.jsonObject).toList() 185 | } 186 | ); 187 | } on Exception catch (exception) { 188 | return TCPResponse( 189 | type: ResponseType.fromRequestType(request.requestType), 190 | status: ResponseStatus.err, 191 | errInfo: exception.toString() 192 | ); 193 | } 194 | } 195 | 196 | Future onFindFile(TCPRequest request, Socket socket) async { 197 | var hasFile = await DataBaseHelper().findFile(fileMd5: request.body['filemd5'] as String); 198 | return TCPResponse( 199 | type: ResponseType.fromRequestType(request.requestType), 200 | status: hasFile ? ResponseStatus.ok : ResponseStatus.err, 201 | errInfo: hasFile ? null : 'File not found' 202 | ); 203 | } 204 | 205 | Future onFetchFile(TCPRequest request, Socket socket) async { 206 | try { 207 | var filePath = await DataBaseHelper().fetchFilePath(msgMd5: request.body['msgmd5'] as String); 208 | var file = File(filePath); 209 | return TCPResponse( 210 | type: ResponseType.fromRequestType(request.requestType), 211 | status: ResponseStatus.ok, 212 | payload: file 213 | ); 214 | } on Exception catch (exception) { 215 | return TCPResponse( 216 | type: ResponseType.fromRequestType(request.requestType), 217 | status: ResponseStatus.err, 218 | errInfo: exception.toString() 219 | ); 220 | } 221 | } 222 | 223 | Future onSearchUser(TCPRequest request, Socket socket) async { 224 | try { 225 | var userInfo = await DataBaseHelper().fetchUserInfoViaUsername(username: request.body['username'] as String); 226 | return TCPResponse( 227 | type: ResponseType.fromRequestType(request.requestType), 228 | status: ResponseStatus.ok, 229 | body: userInfo.jsonObject 230 | ); 231 | } on Exception catch (exception) { 232 | return TCPResponse( 233 | type: ResponseType.fromRequestType(request.requestType), 234 | status: ResponseStatus.err, 235 | errInfo: exception.toString() 236 | ); 237 | } 238 | } 239 | 240 | Future onAddContact(TCPRequest request, Socket socket) async { 241 | try { 242 | await DataBaseHelper().addContact(tokenID: request.tokenID, userID: request.body['userid'] as int); 243 | return TCPResponse( 244 | type: ResponseType.fromRequestType(request.requestType), 245 | status: ResponseStatus.ok 246 | ); 247 | } on Exception catch (exception) { 248 | return TCPResponse( 249 | type: ResponseType.fromRequestType(request.requestType), 250 | status: ResponseStatus.err, 251 | errInfo: exception.toString() 252 | ); 253 | } 254 | } 255 | 256 | Future onFetchContact(TCPRequest request, Socket socket) async { 257 | try { 258 | var contacts = await DataBaseHelper().fetchContact(tokenID: request.tokenID); 259 | var pendingContacts = await DataBaseHelper().fetchPendingContacts(tokenID: request.tokenID); 260 | var requestingContacts = await DataBaseHelper().fetchRequestingContacts(tokenID: request.tokenID); 261 | return TCPResponse( 262 | type: ResponseType.fromRequestType(request.requestType), 263 | status: ResponseStatus.ok, 264 | body: { 265 | "contacts": contacts.map((e) => e.jsonObject).toList(), 266 | "pending": pendingContacts.map((e) => e.jsonObject).toList(), 267 | "requesting": requestingContacts.map((e) => e.jsonObject).toList() 268 | } 269 | ); 270 | } on Exception catch (exception) { 271 | return TCPResponse( 272 | type: ResponseType.fromRequestType(request.requestType), 273 | status: ResponseStatus.err, 274 | errInfo: exception.toString() 275 | ); 276 | } 277 | } 278 | 279 | void onAckFetch(TCPRequest request, Socket socket) async { 280 | //Update Fetch Histories 281 | await DataBaseHelper().setFetchHistoryFor( 282 | tokenID: request.tokenID, 283 | newTimeStamp: request.body['timestamp'] as int, 284 | ); 285 | } 286 | 287 | Future onUnknownRequest(TCPRequest request, Socket socket) async { 288 | return TCPResponse( 289 | type: ResponseType.fromRequestType(request.requestType), 290 | status: ResponseStatus.err, 291 | errInfo: 'Unkown request' 292 | ); 293 | } -------------------------------------------------------------------------------- /lib/tcpcontroller/controller.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-08 15:10:04 4 | * @LastEditTime : 2022-10-22 21:20:57 5 | * @Description : 6 | */ 7 | 8 | import 'dart:async'; 9 | import 'dart:io'; 10 | import 'dart:typed_data'; 11 | 12 | import 'package:async/async.dart'; 13 | import 'package:tcp_server/tcpcontroller/request.dart'; 14 | import 'package:tcp_server/tcpcontroller/response.dart'; 15 | 16 | class TCPController { 17 | final Socket socket; 18 | 19 | //Stores the incoming bytes of the TCP connection temporarily 20 | final List buffer = []; 21 | 22 | //Byte length for json object 23 | int requestLength = 0; 24 | //Byte length for subsequent data of the json object 25 | int payloadLength = 0; 26 | 27 | int _fileCounter = 0; 28 | 29 | //Construct a stream which emits events on intact requests 30 | final StreamController> _requestRawStreamController = StreamController(); 31 | final StreamController _payloadRawStreamController = StreamController(); 32 | 33 | //Construct a payload stream which forward the incoming byte into temp file 34 | StreamController> _payloadPullStreamController = StreamController()..close(); 35 | 36 | //Provide a request stream for caller functions to listen on 37 | final StreamController _requestStreamController = StreamController(); 38 | Stream? _requestStreamBroadcast; 39 | Stream get requestStreamBroadcast { 40 | _requestStreamBroadcast ??= _requestStreamController.stream.asBroadcastStream(); 41 | return _requestStreamBroadcast!; 42 | } 43 | 44 | //Provide a post stream for caller functions to push to 45 | final StreamController _responseStreamController = StreamController(); 46 | StreamSink get outStream => _responseStreamController; 47 | Stream? _responseStreamBroadcast; 48 | Stream get responseStreamBroadcast { 49 | _responseStreamBroadcast ??= _responseStreamController.stream.asBroadcastStream(); 50 | return _responseStreamBroadcast!; 51 | } 52 | 53 | TCPController({ 54 | required this.socket 55 | }) { 56 | print('[L] [CONNECTED]-----------------------'); 57 | print('[L] Connection Established'); 58 | print('[L] Remote: ${socket.remoteAddress}:${socket.remotePort}'); 59 | print('[L] Local: ${socket.address}:${socket.port}'); 60 | Future(() async { 61 | try { 62 | await for(var request in socket) { 63 | _pullRequest(request); 64 | await Future.delayed(const Duration(microseconds: 0)); 65 | } 66 | } catch (e) { 67 | _requestStreamController.addError(e); 68 | _responseStreamController.addError(e); 69 | } 70 | }).then((_) { 71 | print('[L] [CLOSED ]-----------------------'); 72 | print('[L] Connection closed: ${socket.address}:${socket.port}'); 73 | _requestStreamController.close(); 74 | _responseStreamController.close(); 75 | }); 76 | // socket.listen( 77 | // _pullRequest, 78 | // onError: (e) { 79 | // print(e); 80 | // _requestStreamController.addError(e); 81 | // }, 82 | // onDone: () { 83 | // print('[L] [CLOSED ]-----------------------'); 84 | // print('[L] Connection closed: ${socket.address}:${socket.port}<-${socket.remoteAddress}:${socket.remotePort}'); 85 | // _requestStreamController.close(); 86 | // }, 87 | // cancelOnError: true, 88 | // ); 89 | //This future never ends, would that be bothersome? 90 | Future(() async { 91 | try{ 92 | await for(var response in responseStreamBroadcast) { 93 | await socket.addStream(response.stream); 94 | } 95 | } catch (e) { 96 | print('[E] [EXCEPTION]-----------------------'); 97 | print('[E] Adding bytes to socket stream failed'); 98 | await socket.flush(); 99 | socket.close(); 100 | } 101 | }); 102 | //This one will fail if two request are handled simultaneously, which cause a stream 103 | //adding to the socket which was already added by the previous stream 104 | // _outStreamController.stream.listen((response) async { 105 | // await socket.addStream(response.stream); 106 | // }); 107 | Future(() async { 108 | var requestQueue = StreamQueue(_requestRawStreamController.stream); 109 | var payloadQueue = StreamQueue(_payloadRawStreamController.stream); 110 | while(await Future(() => !_requestRawStreamController.isClosed && !_payloadRawStreamController.isClosed)) { 111 | var request = await requestQueue.next; 112 | var payload = await payloadQueue.next; 113 | await _pushRequest(requestBytes: request, tempFile: payload); 114 | } 115 | requestQueue.cancel(); 116 | payloadQueue.cancel(); 117 | }); 118 | } 119 | 120 | //Listen to the incoming stream and emits event whenever there is a intact request 121 | void _pullRequest(Uint8List fetchedData) { 122 | //Put incoming data into buffer 123 | buffer.addAll(fetchedData); 124 | //Consume buffer until it's not enough for first 8 byte of a message 125 | while(true) { 126 | if(requestLength == 0 && payloadLength == 0 && _payloadPullStreamController.isClosed) { 127 | //New request 128 | if(buffer.length >= 12) { 129 | //Buffered data has more than 8 bytes, enough to read request length and body length 130 | requestLength = Uint8List.fromList(buffer.sublist(0, 4)).buffer.asInt32List()[0]; 131 | payloadLength = Uint8List.fromList(buffer.sublist(4, 12)).buffer.asInt64List()[0]; 132 | //Clear the length indicator bytes 133 | buffer.removeRange(0, 12); 134 | //Initialize payload transmission controller 135 | _payloadPullStreamController = StreamController(); 136 | //Create a future that listens to the status of the payload transmission 137 | () { 138 | var payloadPullStream = _payloadPullStreamController.stream; 139 | var tempFile = File('${Directory.current.path}/.data/.tmp/${DateTime.now().microsecondsSinceEpoch}$_fileCounter')..createSync(); 140 | _fileCounter += 1; 141 | _fileCounter %= 1000; 142 | Future(() async { 143 | await for(var data in payloadPullStream) { 144 | await tempFile.writeAsBytes(data, mode: FileMode.writeOnlyAppend); 145 | } 146 | _payloadRawStreamController.add(tempFile); 147 | }); 148 | }(); 149 | } 150 | else { 151 | //Buffered data is not long enough 152 | //Do nothing 153 | break; 154 | } 155 | } 156 | else { 157 | //Currently awaiting full transmission 158 | if(requestLength > 0) { 159 | //Currently processing on a request 160 | if(buffer.length >= requestLength) { 161 | //Got intact request json 162 | //Emit request buffer through stream 163 | _requestRawStreamController.add(buffer.sublist(0, requestLength)); 164 | //Remove proccessed buffer 165 | buffer.removeRange(0, requestLength); 166 | //Clear awaiting request length 167 | requestLength = 0; 168 | } 169 | else { 170 | //Got part of request json 171 | //do nothing 172 | break; 173 | } 174 | } 175 | else { 176 | //Currently processing on a payload 177 | if(buffer.length >= payloadLength) { 178 | //Last few bytes to emit 179 | //Send the last few bytes to stream 180 | _payloadPullStreamController.add(buffer.sublist(0, payloadLength)); 181 | //Clear buffer 182 | buffer.removeRange(0, payloadLength); 183 | //Set payload length to zero 184 | payloadLength = 0; 185 | //Close the payload transmission stream 186 | _payloadPullStreamController.close(); 187 | } 188 | else { 189 | //Part of payload 190 | //Transmit all to stream 191 | _payloadPullStreamController.add([...buffer]); 192 | //Reduce payload bytes left 193 | payloadLength -= buffer.length; 194 | //Clear buffer 195 | buffer.clear(); 196 | //Exit and wait for another submit 197 | break; 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | Future _pushRequest({ 205 | required List requestBytes, 206 | required File tempFile 207 | }) async { 208 | try{ 209 | _requestStreamController.add(TCPRequest(requestBytes, tempFile)); 210 | } catch (e) { 211 | print('[E] [EXCEPTION]-----------------------'); 212 | print('[E] Adding bytes to request stream failed'); 213 | } 214 | } 215 | } -------------------------------------------------------------------------------- /lib/tcpcontroller/payload/identity.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-08 16:16:10 4 | * @LastEditTime : 2022-10-08 22:36:19 5 | * @Description : 6 | */ 7 | 8 | class UserIdentity { 9 | final Map _data; 10 | 11 | UserIdentity({ 12 | required String userName, 13 | required String userPasswdEncoded, 14 | String? userPasswdEncodedNew 15 | }): _data = { 16 | "username": userName, 17 | "passwd": userPasswdEncoded, 18 | "newPasswd": userPasswdEncodedNew 19 | }; 20 | UserIdentity.fromJSONObject(Map data): _data = data; 21 | 22 | String get userName => _data['username'] as String; 23 | String get userPasswd => _data['passwd'] as String; 24 | String? get userPasswdNew => _data['newPasswd'] as String?; 25 | Map get jsonObject => _data; 26 | } -------------------------------------------------------------------------------- /lib/tcpcontroller/payload/message.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-08 16:16:19 4 | * @LastEditTime : 2022-10-09 14:53:11 5 | * @Description : Message Info Payload 6 | */ 7 | 8 | import 'package:crypto/crypto.dart'; 9 | import 'package:tcp_server/utils/typeconverter.dart'; 10 | 11 | enum MessageType { 12 | plaintext('plaintext'), 13 | file('file'), 14 | image('image'); 15 | 16 | factory MessageType.fromStringLiteral(String value) { 17 | return MessageType.values.firstWhere((element) => element._value == value); 18 | } 19 | const MessageType(String value): _value = value; 20 | final String _value; 21 | String get literal => _value; 22 | } 23 | 24 | class Message { 25 | final Map _data; 26 | 27 | Message({ 28 | required int userid, 29 | required int targetid, 30 | required MessageType contenttype, 31 | required String content, 32 | required int timestamp, 33 | String? md5encoded, 34 | String? filemd5 35 | }): _data = { 36 | "userid": userid, 37 | "targetid": targetid, 38 | "contenttype": contenttype.literal, 39 | "content": content, 40 | "timestamp": timestamp, 41 | "md5encoded": md5encoded ?? md5.convert( 42 | intToUint8List(userid) 43 | ..addAll(intToUint8List(targetid)) 44 | ..addAll(intToUint8List(timestamp)) 45 | ..addAll(content.codeUnits) 46 | ).toString(), 47 | "filemd5": filemd5 48 | }; 49 | Message.fromJSONObject(Map data): _data = data; 50 | 51 | int get senderID => _data['userid'] as int; 52 | int get receiverID => _data['targetid'] as int; 53 | MessageType get contentType => MessageType.fromStringLiteral(_data['contenttype'] as String); 54 | String get content => _data['content'] as String; 55 | int get timestamp => _data['timestamp'] as int; 56 | String get md5encoded => _data['md5encoded'] as String; 57 | String? get fileMd5 => _data['filemd5'] as String?; 58 | Map get jsonObject => _data; 59 | } -------------------------------------------------------------------------------- /lib/tcpcontroller/payload/userinfo.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-08 16:15:17 4 | * @LastEditTime : 2022-10-08 22:35:53 5 | * @Description : User Info Payload 6 | */ 7 | 8 | class UserInfo { 9 | final Map _data; 10 | 11 | UserInfo({ 12 | required int userID, 13 | required String userName, 14 | String? userAvatar 15 | }): _data = { 16 | "userid": userID, 17 | "username": userName, 18 | "avatar": userAvatar 19 | }; 20 | UserInfo.fromJSONObject(Map data): _data = data; 21 | 22 | int get userID => _data['userid'] as int; 23 | String get userName => _data['username'] as String; 24 | String? get userAvatar => _data['avatar'] as String?; 25 | Map get jsonObject => _data; 26 | } -------------------------------------------------------------------------------- /lib/tcpcontroller/request.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-08 15:14:26 4 | * @LastEditTime : 2022-10-22 20:54:40 5 | * @Description : 6 | */ 7 | import 'dart:convert'; 8 | import 'dart:io'; 9 | 10 | enum RequestType { 11 | checkState ('STATE'), //Check login state for device token 12 | register ('REGISTER'), //Register new user 13 | login ('LOGIN'), //Login via username and password 14 | logout ('LOGOUT'), //Logout for current device token 15 | profile ('PROFILE'), //Fetch current logged in user profile 16 | modifyPassword('MODIFYPASSWD'), //Modify user password 17 | modifyProfile ('MODIFYPROFILE'), //Modify user profile 18 | sendMessage ('SENDMSG'), //Send message 19 | ackFetch ('ACKFETCH'), //Ack fetched messages, update fetch history 20 | fetchMessage ('FETCHMSG'), //Fetch message 21 | findFile ('FINDFILE'), //Find file by md5 before transmitting the file 22 | fetchFile ('FETCHFILE'), //Fetch file and file md5 by message md5 23 | searchUser ('SEARCHUSR'), //Search username and userid by username 24 | addContact ('ADDCONTACT'), //Add one-way relation to a user 25 | fetchContact ('FETCHCONTACT'), //Fetch all contacts, including requesting and pending 26 | unknown ('UNKNOWN'); //Wrong command 27 | 28 | const RequestType(String value): _value = value; 29 | final String _value; 30 | String get value => _value; 31 | 32 | //Construct the enum type by value 33 | factory RequestType.fromValue(String value) { 34 | return RequestType.values.firstWhere((element) => element._value == value, orElse: () => RequestType.unknown); 35 | } 36 | } 37 | 38 | //Object wrapper for tcp request string 39 | class TCPRequest { 40 | final Map _data; 41 | File? payload; 42 | 43 | TCPRequest(List data, this.payload): _data = jsonDecode(String.fromCharCodes(data)); 44 | TCPRequest.fromData({ 45 | required RequestType type, 46 | required Map body, 47 | required int? tokenID, 48 | this.payload 49 | }): _data = { 50 | 'request': type.value, 51 | 'tokenid': tokenID, 52 | 'body': body 53 | }; 54 | TCPRequest.none(): _data = {}; 55 | 56 | String get toJSON => jsonEncode(_data); 57 | RequestType get requestType => RequestType.fromValue(_data['request'] as String); 58 | int? get tokenID => _data['tokenid'] as int?; 59 | set tokenID(int? t) => _data['tokenid'] = t; 60 | Map get body => _data['body'] as Map? ?? {}; 61 | } -------------------------------------------------------------------------------- /lib/tcpcontroller/response.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-08 22:40:47 4 | * @LastEditTime : 2022-10-18 14:44:49 5 | * @Description : 6 | */ 7 | 8 | import 'dart:convert'; 9 | import 'dart:io'; 10 | import 'dart:typed_data'; 11 | 12 | import 'package:tcp_server/tcpcontroller/request.dart'; 13 | 14 | enum ResponseType { 15 | token ('TOKEN'), //Only exists when server is sending message 16 | checkState ('STATE'), //Check login state for device token 17 | register ('REGISTER'), //Register new user 18 | login ('LOGIN'), //Login via username and password 19 | logout ('LOGOUT'), //Logout for current device token 20 | profile ('PROFILE'), //Fetch current logged in user profile 21 | modifyPassword('MODIFYPASSWD'), //Modify user password 22 | modifyProfile ('MODIFYPROFILE'), //Modify user profile 23 | sendMessage ('SENDMSG'), //Send message 24 | forwardMessage('FORWARDMSG'), //Forward message 25 | fetchMessage ('FETCHMSG'), //Fetch message 26 | findFile ('FINDFILE'), //Find file by md5 before transmitting the file 27 | fetchFile ('FETCHFILE'), //Fetch file and file md5 by message md5 28 | searchUser ('SEARCHUSR'), //Search username and userid by username 29 | addContact ('ADDCONTACT'), //Add one-way relation to a user 30 | fetchContact ('FETCHCONTACT'), //Fetch all contacts, including requesting and pending 31 | unknown ('UNKNOWN'); //Wrong command 32 | 33 | const ResponseType(String value): _value = value; 34 | final String _value; 35 | String get value => _value; 36 | 37 | //Construct the enum type by value 38 | factory ResponseType.fromValue(String value) { 39 | return ResponseType.values.firstWhere((element) => element._value == value, orElse: () => ResponseType.unknown); 40 | } 41 | factory ResponseType.fromRequestType(RequestType type) { 42 | return ResponseType.values.firstWhere((element) => element._value == type.value, orElse: () => ResponseType.unknown); 43 | } 44 | } 45 | 46 | enum ResponseStatus { 47 | ok('OK'), 48 | err('ERR'); 49 | 50 | final String _value; 51 | const ResponseStatus(String v): _value = v; 52 | 53 | String get value => _value; 54 | } 55 | 56 | class TCPResponse { 57 | final String responseJson; 58 | final File? payloadFile; 59 | 60 | TCPResponse({ 61 | required ResponseType type, 62 | required ResponseStatus status, 63 | Map? body, 64 | String? errInfo, 65 | File? payload 66 | }): 67 | responseJson = jsonEncode({ 68 | "response": type.value, 69 | "status": status.value, 70 | "info": errInfo, 71 | "body": body, 72 | }), 73 | payloadFile = payload; 74 | 75 | int get responseLength => responseJson.codeUnits.length; 76 | int get payloadLength => payloadFile?.lengthSync() ?? 0; 77 | Stream> get stream async* { 78 | yield Uint8List(4)..buffer.asInt32List()[0] = responseLength; 79 | yield Uint8List(8)..buffer.asInt64List()[0] = payloadLength; 80 | yield Uint8List.fromList(responseJson.codeUnits); 81 | if(payloadFile != null) { 82 | yield await payloadFile!.readAsBytes(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /lib/utils/typeconverter.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : Linloir 3 | * @Date : 2022-10-08 17:21:45 4 | * @LastEditTime : 2022-10-08 17:29:21 5 | * @Description : Type Converters 6 | */ 7 | 8 | import 'dart:typed_data'; 9 | 10 | import 'package:convert/convert.dart'; 11 | 12 | String uint8ListToHexString(Uint8List data) { 13 | return data.buffer.asUint8List().map((e) => e.toRadixString(16).padLeft(2, '0')).join(); 14 | } 15 | 16 | Uint8List hexToUint8List(String string) { 17 | return Uint8List.fromList(hex.decode(string)); 18 | } 19 | 20 | Uint8List intToUint8List(int value) { 21 | return Uint8List(4)..buffer.asInt32List()[0] = value; 22 | } -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "49.0.0" 11 | analyzer: 12 | dependency: transitive 13 | description: 14 | name: analyzer 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "5.1.0" 18 | args: 19 | dependency: transitive 20 | description: 21 | name: args 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "2.3.1" 25 | async: 26 | dependency: "direct main" 27 | description: 28 | name: async 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "2.9.0" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "2.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.16.0" 46 | convert: 47 | dependency: "direct main" 48 | description: 49 | name: convert 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "3.0.2" 53 | coverage: 54 | dependency: transitive 55 | description: 56 | name: coverage 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "1.6.1" 60 | crypto: 61 | dependency: "direct main" 62 | description: 63 | name: crypto 64 | url: "https://pub.flutter-io.cn" 65 | source: hosted 66 | version: "3.0.2" 67 | ffi: 68 | dependency: transitive 69 | description: 70 | name: ffi 71 | url: "https://pub.flutter-io.cn" 72 | source: hosted 73 | version: "2.0.1" 74 | file: 75 | dependency: transitive 76 | description: 77 | name: file 78 | url: "https://pub.flutter-io.cn" 79 | source: hosted 80 | version: "6.1.4" 81 | frontend_server_client: 82 | dependency: transitive 83 | description: 84 | name: frontend_server_client 85 | url: "https://pub.flutter-io.cn" 86 | source: hosted 87 | version: "3.0.0" 88 | glob: 89 | dependency: transitive 90 | description: 91 | name: glob 92 | url: "https://pub.flutter-io.cn" 93 | source: hosted 94 | version: "2.1.0" 95 | http_multi_server: 96 | dependency: transitive 97 | description: 98 | name: http_multi_server 99 | url: "https://pub.flutter-io.cn" 100 | source: hosted 101 | version: "3.2.1" 102 | http_parser: 103 | dependency: transitive 104 | description: 105 | name: http_parser 106 | url: "https://pub.flutter-io.cn" 107 | source: hosted 108 | version: "4.0.1" 109 | io: 110 | dependency: transitive 111 | description: 112 | name: io 113 | url: "https://pub.flutter-io.cn" 114 | source: hosted 115 | version: "1.0.3" 116 | js: 117 | dependency: transitive 118 | description: 119 | name: js 120 | url: "https://pub.flutter-io.cn" 121 | source: hosted 122 | version: "0.6.4" 123 | lints: 124 | dependency: "direct dev" 125 | description: 126 | name: lints 127 | url: "https://pub.flutter-io.cn" 128 | source: hosted 129 | version: "2.0.0" 130 | logging: 131 | dependency: transitive 132 | description: 133 | name: logging 134 | url: "https://pub.flutter-io.cn" 135 | source: hosted 136 | version: "1.1.0" 137 | matcher: 138 | dependency: transitive 139 | description: 140 | name: matcher 141 | url: "https://pub.flutter-io.cn" 142 | source: hosted 143 | version: "0.12.12" 144 | meta: 145 | dependency: transitive 146 | description: 147 | name: meta 148 | url: "https://pub.flutter-io.cn" 149 | source: hosted 150 | version: "1.8.0" 151 | mime: 152 | dependency: transitive 153 | description: 154 | name: mime 155 | url: "https://pub.flutter-io.cn" 156 | source: hosted 157 | version: "1.0.2" 158 | node_preamble: 159 | dependency: transitive 160 | description: 161 | name: node_preamble 162 | url: "https://pub.flutter-io.cn" 163 | source: hosted 164 | version: "2.0.1" 165 | package_config: 166 | dependency: transitive 167 | description: 168 | name: package_config 169 | url: "https://pub.flutter-io.cn" 170 | source: hosted 171 | version: "2.1.0" 172 | path: 173 | dependency: transitive 174 | description: 175 | name: path 176 | url: "https://pub.flutter-io.cn" 177 | source: hosted 178 | version: "1.8.2" 179 | pool: 180 | dependency: transitive 181 | description: 182 | name: pool 183 | url: "https://pub.flutter-io.cn" 184 | source: hosted 185 | version: "1.5.1" 186 | pub_semver: 187 | dependency: transitive 188 | description: 189 | name: pub_semver 190 | url: "https://pub.flutter-io.cn" 191 | source: hosted 192 | version: "2.1.1" 193 | shelf: 194 | dependency: transitive 195 | description: 196 | name: shelf 197 | url: "https://pub.flutter-io.cn" 198 | source: hosted 199 | version: "1.4.0" 200 | shelf_packages_handler: 201 | dependency: transitive 202 | description: 203 | name: shelf_packages_handler 204 | url: "https://pub.flutter-io.cn" 205 | source: hosted 206 | version: "3.0.1" 207 | shelf_static: 208 | dependency: transitive 209 | description: 210 | name: shelf_static 211 | url: "https://pub.flutter-io.cn" 212 | source: hosted 213 | version: "1.1.1" 214 | shelf_web_socket: 215 | dependency: transitive 216 | description: 217 | name: shelf_web_socket 218 | url: "https://pub.flutter-io.cn" 219 | source: hosted 220 | version: "1.0.2" 221 | source_map_stack_trace: 222 | dependency: transitive 223 | description: 224 | name: source_map_stack_trace 225 | url: "https://pub.flutter-io.cn" 226 | source: hosted 227 | version: "2.1.0" 228 | source_maps: 229 | dependency: transitive 230 | description: 231 | name: source_maps 232 | url: "https://pub.flutter-io.cn" 233 | source: hosted 234 | version: "0.10.10" 235 | source_span: 236 | dependency: transitive 237 | description: 238 | name: source_span 239 | url: "https://pub.flutter-io.cn" 240 | source: hosted 241 | version: "1.9.1" 242 | sqflite_common: 243 | dependency: "direct main" 244 | description: 245 | name: sqflite_common 246 | url: "https://pub.flutter-io.cn" 247 | source: hosted 248 | version: "2.3.0" 249 | sqflite_common_ffi: 250 | dependency: "direct main" 251 | description: 252 | name: sqflite_common_ffi 253 | url: "https://pub.flutter-io.cn" 254 | source: hosted 255 | version: "2.1.1+1" 256 | sqlite3: 257 | dependency: transitive 258 | description: 259 | name: sqlite3 260 | url: "https://pub.flutter-io.cn" 261 | source: hosted 262 | version: "1.9.0" 263 | stack_trace: 264 | dependency: transitive 265 | description: 266 | name: stack_trace 267 | url: "https://pub.flutter-io.cn" 268 | source: hosted 269 | version: "1.10.0" 270 | stream_channel: 271 | dependency: transitive 272 | description: 273 | name: stream_channel 274 | url: "https://pub.flutter-io.cn" 275 | source: hosted 276 | version: "2.1.1" 277 | string_scanner: 278 | dependency: transitive 279 | description: 280 | name: string_scanner 281 | url: "https://pub.flutter-io.cn" 282 | source: hosted 283 | version: "1.1.1" 284 | synchronized: 285 | dependency: transitive 286 | description: 287 | name: synchronized 288 | url: "https://pub.flutter-io.cn" 289 | source: hosted 290 | version: "3.0.0+3" 291 | term_glyph: 292 | dependency: transitive 293 | description: 294 | name: term_glyph 295 | url: "https://pub.flutter-io.cn" 296 | source: hosted 297 | version: "1.2.1" 298 | test: 299 | dependency: "direct dev" 300 | description: 301 | name: test 302 | url: "https://pub.flutter-io.cn" 303 | source: hosted 304 | version: "1.21.6" 305 | test_api: 306 | dependency: transitive 307 | description: 308 | name: test_api 309 | url: "https://pub.flutter-io.cn" 310 | source: hosted 311 | version: "0.4.14" 312 | test_core: 313 | dependency: transitive 314 | description: 315 | name: test_core 316 | url: "https://pub.flutter-io.cn" 317 | source: hosted 318 | version: "0.4.18" 319 | typed_data: 320 | dependency: transitive 321 | description: 322 | name: typed_data 323 | url: "https://pub.flutter-io.cn" 324 | source: hosted 325 | version: "1.3.1" 326 | udp: 327 | dependency: "direct main" 328 | description: 329 | name: udp 330 | url: "https://pub.flutter-io.cn" 331 | source: hosted 332 | version: "5.0.3" 333 | vm_service: 334 | dependency: transitive 335 | description: 336 | name: vm_service 337 | url: "https://pub.flutter-io.cn" 338 | source: hosted 339 | version: "9.4.0" 340 | watcher: 341 | dependency: transitive 342 | description: 343 | name: watcher 344 | url: "https://pub.flutter-io.cn" 345 | source: hosted 346 | version: "1.0.1" 347 | web_socket_channel: 348 | dependency: transitive 349 | description: 350 | name: web_socket_channel 351 | url: "https://pub.flutter-io.cn" 352 | source: hosted 353 | version: "2.2.0" 354 | webkit_inspection_protocol: 355 | dependency: transitive 356 | description: 357 | name: webkit_inspection_protocol 358 | url: "https://pub.flutter-io.cn" 359 | source: hosted 360 | version: "1.2.0" 361 | yaml: 362 | dependency: transitive 363 | description: 364 | name: yaml 365 | url: "https://pub.flutter-io.cn" 366 | source: hosted 367 | version: "3.1.1" 368 | sdks: 369 | dart: ">=2.18.2 <3.0.0" 370 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: tcp_server 2 | description: A sample command-line application. 3 | version: 1.0.0 4 | # homepage: https://www.example.com 5 | 6 | environment: 7 | sdk: '>=2.18.2 <3.0.0' 8 | 9 | dependencies: 10 | udp: ^5.0.3 11 | sqflite_common_ffi: ^2.1.1+1 12 | sqflite_common: ^2.3.0 13 | crypto: ^3.0.2 14 | convert: ^3.0.2 15 | async: ^2.9.0 16 | 17 | # dependencies: 18 | # path: ^1.8.0 19 | 20 | dev_dependencies: 21 | lints: ^2.0.0 22 | test: ^1.16.0 23 | --------------------------------------------------------------------------------