Compare commits

...

270 Commits

Author SHA1 Message Date
1e617bb8be Move ANSI* commands to BBS/
Some checks failed
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m44s
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
2024-05-28 12:43:58 +10:00
b43784574c BBS ported from vbbs 2024-05-28 12:37:52 +10:00
364815e8af Setup to present different mail bundle types 2024-05-28 12:23:59 +10:00
3d43a256ba Added DynamicItem and PacketDump debug utilities 2024-05-28 10:44:33 +10:00
b460cd0196 Fix display of addresses in pkt dump 2024-05-28 09:38:55 +10:00
643f1197d6 Fix when we use newFTN, find the zone/domain if it is provided. Fix packet creation error, where Address::ftn depends on zone_id
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-27 22:22:38 +10:00
7ef9f2dbd0 Fix/optimise address creation/editing via System AKAs
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 40s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m46s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-27 21:42:03 +10:00
b102fc4d2a Fix creating a new discovered address and setting validated to true
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m42s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-27 18:04:04 +10:00
e15331ec35 No function changes. Cleanup console command cleanup
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 40s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-27 15:08:39 +10:00
3a594acc03 Fix edit of AKAs for a system. Fix css usage of label for= for the yes/no radio inputs
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m41s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-27 10:48:50 +10:00
800593d034 Optimise Zone::class to identify region/hosts/hubs 2024-05-27 10:47:42 +10:00
65b2a2d519 Fix from bug introduct by 7e0178d for echomails
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m44s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-26 22:08:39 +10:00
bf21671a1f Show system name on AbsentNodes echomail
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 43s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 12s
2024-05-26 22:01:35 +10:00
fe18968c57 Show node status with Hub Stats 2024-05-26 21:50:13 +10:00
b6639c7bfc When de-listing, remove unsent items and unsubscribe from file/echo areas.
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m48s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-26 21:41:02 +10:00
7e0178d183 Fix routed netmails being packed for the hub, not the destination. Added some logging for idle netmails/emails 2024-05-26 20:55:39 +10:00
03bfc9dbfc Fix path on rejected echomails, change layout of message_path quoting original message and control lines
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m42s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-26 12:35:13 +10:00
77b9bb30c4 Another fix to make sure site generated echomail is not exported to ourselfs
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m41s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-26 12:08:01 +10:00
f8cb6ccc37 Automatically mark idle nodes HOLD/DOWN/DE-LIST. Automatically validate presented addresses.
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-25 22:31:42 +10:00
0e1d5b3239 Fix text lable for Nodelist file 2024-05-25 20:03:23 +10:00
9f3b9f692a Dont show a MSGID kludge in test messages if there isnt one
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m44s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-25 14:12:04 +10:00
391e9e1e39 Fix TicProcess, save() returns a bool, not the object
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m45s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-25 11:09:34 +10:00
3555e5a91c Dont cache the mail:send query
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m42s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-25 10:33:35 +10:00
2f24e13940 Dont add a MSGID kludge if there isnt one
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 41s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m44s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-25 00:43:44 +10:00
87f495b326 Add nodelist segment creation
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-24 17:09:10 +10:00
1615b413a7 Assume f0 for DNS queries that dont pass an f in the query
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m40s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-24 12:34:28 +10:00
86c27a3f17 Show users hub connection details for their domains
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 36s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-24 11:04:25 +10:00
a687b5fd1c Change looking for mail waiting for downlinks() instead of children() 2024-05-24 09:28:17 +10:00
27956146e3 Change domain view last seen to show last time an echomail came from that system
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 40s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-23 23:28:42 +10:00
a547e29e56 Sometimes errors have URLs, so enable them to be resolved 2024-05-23 22:38:03 +10:00
de34052c3b Add constraint for hubs, which must have the same host and region for a zone
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 40s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m48s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-23 21:46:26 +10:00
cb63ec50d2 Dont cache when doing debug:zone:check 2024-05-23 21:31:17 +10:00
8b00d29db3 Update mailist to look for deleted FTNs as well
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m41s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-23 20:30:03 +10:00
f082bb0ebd Dont record us in seenby/path for local messages. Update echomail display to know if an exchomail seenby has collected the message
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m46s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-23 20:12:21 +10:00
4f8448563d Fix for when adding our address to path for outgoing echomails - introduced in 5fc6906
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 41s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m46s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-23 17:38:18 +10:00
710adad634 Show only validated addresses with packets for packet:system 2024-05-23 17:37:46 +10:00
5fc69067fb Fix sorting of seenby/path items in echomail/netmail. Add rogue_seenby to seenby in echomail 2024-05-23 17:37:46 +10:00
dc212d35fb Work to handle grunged packets as well as look for tearline/tagline/orgin line from the end of the content
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 40s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m44s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-22 23:24:29 +10:00
3ce6a8ed61 Record the AKAs presented 2024-05-22 22:12:38 +10:00
b398163cfd We need to use EncodeUTF our mail objects while passing them to the queue
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 41s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m42s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-22 21:37:58 +10:00
a5e9a28673 Added PRODUCT_NAME_SHORT and updated PID/TID kludge to include git hash
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 41s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-22 15:31:10 +10:00
49e40f4fb8 Generated mail from the hub wont have $model->errors defined 2024-05-22 09:21:59 +10:00
924d760c79 Fix FSC45 packets, point_id was still being validated the old way 2024-05-22 09:21:59 +10:00
58dc090c83 Use packets domain for packet validation, not remote sytems address 2024-05-22 09:21:59 +10:00
b20878e378 Kludges are not required in messages 2024-05-22 09:21:59 +10:00
b443762739 Fix processing echomail and when mail crc is calculated as it was decompressing the CompressedString 2024-05-22 09:21:59 +10:00
18f5354d0c Mail validation errors is now an object, and must be tested with ->count() 2024-05-22 09:21:59 +10:00
17e3c69f07 Fix for invalid-zone validation comparing a string with an int 2024-05-22 09:21:59 +10:00
51784df6a8 Fix missed Notification using old Message::class 2024-05-22 09:21:59 +10:00
8df6384736 Fixed checking for RESCAN kludge 2024-05-22 09:21:59 +10:00
752462d20f Update job:list, and change "subject" to "jobname" 2024-05-22 09:21:59 +10:00
72d68fa1ab Update SocketClient to support raw IP addresses 2024-05-22 09:21:59 +10:00
aaec5f8f4a Security update enabling update_nn to edit system details 2024-05-22 09:21:59 +10:00
ab2e288f06 More complete rework of packet parsing and packet generation with f279d85 - and testing passes 2024-05-22 09:21:59 +10:00
b30ab2f999 Add github hash VERSION file during build
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 44s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m51s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-05-19 23:30:44 +10:00
b4a42f6780 More update on security, user to edit their own system with 59ec5f5 2024-05-19 23:30:14 +10:00
f279d85b08 More complete rework of packet parsing and packet generation with 29710c 2024-05-19 23:28:45 +10:00
46f52dd56d Only auth AKAs in the same domain as us 2024-05-18 12:26:00 +10:00
59ec5f5a0c Update on security, user to edit their own system 2024-05-18 08:27:17 +10:00
29710c37c2 Complete rework of packet parsing and packet generation 2024-05-17 22:10:54 +10:00
1650d07d5c Fix recording of netmails, when they contain taglines and origin lines 2024-05-16 22:59:37 +10:00
0457b3df25 Resorting methods, no functional changes 2024-05-13 18:55:39 +10:00
731fdb0a44 When decompressing compressed messages, dont barf if we try to decompress the same attribute twice 2024-05-13 17:50:24 +10:00
6216ada5e5 Fix recording of netmails, when they contain taglines and origin lines 2024-05-13 17:50:24 +10:00
b9b5cf4214 Display msgid's smaller for Netmail/Echomail H1 headings 2024-05-13 17:06:51 +10:00
f912e81ee6 Change where processed packets are placed, if fido.packet_keep is true
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 54s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m54s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-12 21:56:46 +10:00
4fe5dc6ad0 Fix for session being reported negative 2024-05-12 21:56:46 +10:00
556b95c7c1 Fix when dispatching packets, and our address object has many relations already loaded, causing memory exhaustion due to recursion 2024-05-12 21:56:46 +10:00
14c505c15b Must not cache the mail waiting queries, otherwise mail/files will be resent in a query loop
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 41s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m42s
Create Docker Image / Final Docker Image Manifest (push) Successful in 12s
2024-05-12 14:56:18 +10:00
dd8558487c Increase default logging to 3 months 2024-05-11 21:20:05 +10:00
3ad20f969b Put back laravel-eloquent-query-cache and remove Caching from previous commit
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m41s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-11 09:10:00 +10:00
4d13199848 Some interface SQL performance improvements 2024-05-11 08:18:57 +10:00
cd2efbd1d4 Added downstream(), and fixed failing tests in RoutingTest
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 42s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m48s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-10 21:33:02 +10:00
edee0643ec Reorder functions, no functional changes 2024-05-09 21:31:50 +10:00
23159d19d5 Rework address roles, making Address::role optional, rework determining uplink/downlinks/parent/children 2024-05-09 21:22:30 +10:00
2765a27db8 Performance fix for address_merge, when there are a log of echomails to move to the new address
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m39s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-06 08:23:07 +10:00
c8ef7d065b Fix address_add validation, missing scoped to zone. Change to use shortform of $request->post() in address_add()
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-05 23:17:29 +10:00
7540ddf8f4 $o is not always defined
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m41s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-05 22:14:33 +10:00
b17fe1d2ee It seems session time is now returning a float, cast it to an int
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m40s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-05 17:41:47 +10:00
5389739920 Enable setting autohold and address validation in web UI
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 41s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m47s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-05 00:10:55 +10:00
92f964f572 Remove static cache from our_address in favour of Cache::class keyed off setup:system_id 2024-04-26 20:31:02 +10:00
9abfd88e3d Fix for AreaSecurity when presented security is null 2024-04-26 20:31:02 +10:00
e9895aee45 Added Echoarea::addresses_active() to find addresses that are connected to the area, and active 2024-04-26 20:31:02 +10:00
79b180f453 Upgrade to Laravel 11, begining of enabling network join functionality, removed QueryCacheable 2024-04-26 20:31:02 +10:00
6e376100a5 Fix System registration form presentation and validation processing
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m42s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-26 12:23:55 +10:00
1a5c1eff7b Move passkey login to a button on the login page
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m41s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-04-25 20:20:43 +10:00
f42fe97902 Add user policy to manage user security
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m40s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-25 16:08:09 +10:00
ac02f37c67 We need to add git to the build for custom composer packages
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m40s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-25 15:56:08 +10:00
6a41536d57 Enable user reseting password
Some checks failed
Create Docker Image / Build Docker Image (x86_64) (push) Failing after 24s
Create Docker Image / Build Docker Image (arm64) (push) Failing after 54s
Create Docker Image / Final Docker Image Manifest (push) Has been skipped
2024-04-25 15:45:05 +10:00
527cc1d4ab Added passkey for logins 2024-04-25 15:45:05 +10:00
d90f431925 Changed layout of system/addedit 2024-04-25 15:45:05 +10:00
ceffc7ff14 Removed unnessary controller functions that just call a view, HTML/CSS consistency updates 2024-04-25 15:44:52 +10:00
001618d719 Move zone:check to debug namespace, add address:check command
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-21 22:10:12 +10:00
bba6f93fbc Code improvement to our_address(), reducing arguments 2024-04-21 21:40:55 +10:00
1c270025cf Move determination of system packet to System::class 2024-04-21 20:40:19 +10:00
8bf58f3daa Added performance indexes for echomails/echoareas
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 36s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-21 15:19:38 +10:00
3f5668292f More optimisations for users dashboard
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 36s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-21 00:28:50 +10:00
20d3776490 Revert 9299697 FIDO_STRICT to default false. Not all FTNs support sending uplinks from a *C address.
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 31s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m36s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-20 09:38:30 +10:00
7b225d8fc0 Attempt to catch dns query failures
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 36s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2024-04-16 22:01:09 +09:30
ff7ab68a54 Added filefix rescan 2024-04-16 22:00:41 +09:30
a2ff2df9f3 Move security evaluations for File/Echoareas back to model 2024-04-16 21:28:35 +09:30
9c9fd84e0a Change layout of topmenu
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 33s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-04-14 20:26:28 +10:00
5cb70da458 Optimise rendering of bbs list 2024-04-14 16:59:06 +10:00
3e561ab068 Optimise rendering of domain list 2024-04-14 16:55:52 +10:00
42cc50512f Fix topmenu dropdown rendering, Recognise POINTs in Address Type 2024-04-14 16:52:47 +10:00
bb42f418e0 Revert part of 9299697, so that our (lowest) address is selected, especially in the case for msgid creation
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 32s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-14 11:03:17 +10:00
ae0bd09a47 Add Debug command Packet:Address
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-14 10:43:07 +10:00
9299697ec1 Fix a caching pollution issue when using static
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 32s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-14 10:23:21 +10:00
bac41969a5 Optimise queries for rendering the users dashboard page
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 32s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-04-14 00:47:08 +10:00
d6e23b9a90 Optimise queries for rendering the about page
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 33s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m38s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-13 22:41:58 +10:00
2edc41b11e Support merging addresses when both src/dst addresses are in the seenby 2024-04-13 20:54:05 +10:00
03ca4c10b1 Only add items to the queue when the queue is empty 2024-04-12 21:22:27 +10:00
1923eb429f Change wording for Internet Address for a System 2024-04-12 20:06:58 +10:00
1e08c2f6f7 Move Domain_Controller::NUMBER_MAX to Address::ADDRESS_FIELD_MAX 2024-04-12 15:29:11 +10:00
77df5746be Added gitea CI/CD configuration
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 31s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-04-10 22:34:30 +10:00
60964e27a7 Attribution to phpStorm 2024-04-10 21:28:45 +10:00
c496d131cf Fix wording on EMSI sessions 2024-03-22 08:45:44 +11:00
2ba656b1d9 Update to php 8.3, change armv7l build to arm64 2024-02-05 23:11:16 +11:00
cb09016539 Partially revert #9cf0f1e so that we create jobs by Address ID, not System ID 2024-01-10 16:58:35 +11:00
0c17391dec Updated parent containers 2024-01-10 16:56:40 +11:00
c1a1797778 If user information is not provided in a system update (for example when users update), then dont zap system_users 2023-12-20 17:18:10 +11:00
9376c6de11 We should check for subscription before checking for permissions 2023-12-20 11:20:48 +11:00
713615d8d5 Revert changes to CompressedString::class, messages were going out base64 encoded and compressed 2023-12-19 16:51:15 +11:00
01107cd3dc Added AnsiLove for rendering messages with ANSI sequences 2023-12-19 15:16:10 +11:00
0e5a04596a Disable armv7l builds 2023-12-19 12:55:27 +11:00
c9d04b64ac Enabled NetmailPolicy, users can see netmail if they are in the seen-by, a ZC or admin 2023-12-19 12:55:27 +11:00
90206f2bb5 Enable admin setting user for a system 2023-12-19 12:55:27 +11:00
ac2ee7df0c Fix for netmail notifications, fftn_id wasnt being set correctly 2023-12-19 09:13:16 +11:00
24f6af3d3b Disabling our routine to set a default on an address, it needs to be improved 2023-12-19 08:54:26 +11:00
9c8e546765 Increase some logging for binkp sessions 2023-12-18 22:44:55 +11:00
13e51724c0 Further enhancement with #9063a2a - to ensure our message addressing and content picks the right address 2023-12-18 20:43:21 +11:00
1ded66990c Enable ZC to see netmail in their zone, and point owners to see their own netmail 2023-12-18 20:43:21 +11:00
6fb7d165ae text-right should be text-end 2023-12-18 20:43:21 +11:00
fd07fb2be7 Include children in netmail for a host 2023-12-18 20:43:21 +11:00
e1c9fa12aa When rethrowing an exception, only include the message 2023-12-18 20:43:21 +11:00
7b9ab388d8 Optimise our use of items waiting and queries used. We are now using a single consistent query for each resource. 2023-12-18 20:43:21 +11:00
7af67de2a8 Fix scenario when creating a new BBS during user registration 'Attempt to read property exists on null' 2023-12-18 20:43:21 +11:00
6d9179ed37 Fix for creating a new area 'Attempt to ready property nodelist on null' 2023-12-18 20:43:21 +11:00
27c050dc38 When we have multiple addresses, add we want a specific address, return the lowest role, or if strict mode enable, return the lowest role that is higher than the target 2023-12-18 20:43:21 +11:00
301fc33d2f HubStats was not limiting the scope to the date, now that the scopeUncollected..() methods dont limit by date (changed in #5a74386) 2023-12-18 20:43:21 +11:00
5d88a5e10e our_address() now takes a domain name - missed it for hub stats 2023-12-18 20:43:21 +11:00
aae551aacf Simplify packet processing. Re-enable pkt processing tests. 2023-12-18 20:43:21 +11:00
26c80dc1c5 Move TIC testing into a file subfolder, so our test folder can have other types of resources 2023-12-18 20:43:21 +11:00
ba0d612889 Change file desc to text, since it can be larger than 255 chars 2023-12-18 20:43:21 +11:00
f6a6c13ca2 NCs should be /0 not /x 2023-12-18 20:43:21 +11:00
b9bc413b05 Fix for finding the TIC origin, it doesnt have a parent 2023-12-18 20:43:21 +11:00
caa6e629f4 Change Address::parent(),Address::children(), improved CI testing 2023-12-18 20:43:21 +11:00
541f612446 Improve our parent/children identification with points, fix our testing that was failing with NULLs and asserted out. Added zone:check so that's its easier to identify parent for FTNs 2023-12-11 18:31:38 +11:00
247cf614f3 Fix log note for PING netmails 2023-12-11 08:48:30 +11:00
ab5476d373 Remove deprecated Protocol:setClient() 2023-12-10 20:44:15 +11:00
0526500ff0 Integrate Mailer::class into System_Log::class, removed Zmodem Server/Client 2023-12-10 20:41:37 +11:00
8fc0336314 Fix for bugs implemented in #e56eca, where the message was including the 3 NULLs at the end and our header is near the end of a buffer read 2023-12-09 13:56:45 +11:00
5a74386f5a Optimisations to uncollected Items, which fix mail:send, which was only being triggered for mail unsent from yesterday 2023-12-08 15:16:49 +11:00
e56ecaa999 Fix for processing packets, where our EOM or EOP is split across buffer reads 2023-12-07 20:19:48 +11:00
ddccc44261 Coverage is now defined in CI/CD yml file 2023-12-07 12:48:26 +11:00
fa2e74eaca Changes to timew() and wtime() to leverage last 2 bits for 4 year timestamp, making msgid checking valid according to FTSC. Added a test suite for timew()/wtime(). 2023-12-07 12:07:11 +11:00
ee15274478 Enhancements to accordion displays, mainly to show chevrons indicating open status 2023-12-04 09:03:54 +11:00
1890b66dc7 Implemented Dynamic Items for data to be sent to polled systems based on data in db, like stats/nodelists 2023-12-03 18:51:46 +11:00
8f3d77b04d Implemented CLI areafix:rescan 2023-12-01 18:14:51 +11:00
049b2c7204 Change BINKP so that we send more packets in the same session, when we have more than msgs_pkt to send 2023-12-01 18:14:07 +11:00
5b7ec1a629 Add missing const to parent() and children() relations 2023-12-01 17:25:01 +11:00
535a082edd Enable overriding the DNS NS hostname 2023-11-28 19:57:47 +11:00
9cf0f1e2f4 Changed AddressPoll unique ID to be system_id, setup for memcached for all micrsoservices 2023-11-27 16:00:02 +11:00
27a3e3e24e Use ObjectIssetFix Trait instead of defining __isset() directly 2023-11-27 15:56:28 +11:00
8590bb8acc Fix the rendering of SEENBY/PATH lines in packets, which should have each new line prefixed with host 2023-11-27 09:00:32 +11:00
a19eaa3291 Enable accordion transition 2023-11-26 17:32:39 +11:00
4a0e6e67fc Added number of netmails/echomails/files processed on status page 2023-11-26 17:32:39 +11:00
1ac3583479 Implemented system heartbeat, to poll systems regularly that we havent heard from 2023-11-26 14:59:05 +11:00
6e7e09ab50 Minor changes to mail:send and job:list rendering 2023-11-26 11:32:21 +11:00
2b2482ba71 Rework crash polling, using optimised scope queries 2023-11-25 21:52:05 +11:00
b5e5decfdf Processing packets on the command line can be archives as well 2023-11-25 09:48:37 +11:00
6abf10ab0b TIC and PKT passwords are case insensitive, so convert them to uppercase when we are using them 2023-11-25 09:47:58 +11:00
4070a060c3 Use created_at not datetime on status, since datetime may be wrong by the sender 2023-11-25 09:29:08 +11:00
a13497df5f Update domain::public() to only show public domains to admins, update status to be consistent with domain::public() 2023-11-25 00:10:21 +11:00
82cee02fa8 Dont included deleted_at items for the status 2023-11-24 23:39:38 +11:00
e337a29003 Determine unsent netmails by their flags, not sent_at for the status 2023-11-24 23:39:38 +11:00
eab35d4c18 On the status page, dont show inactive addresses, zones or domains 2023-11-24 23:39:38 +11:00
4c91ed54c0 First work on a status page showing nodes with uncollected mail 2023-11-24 23:39:38 +11:00
bed5bf8acc Binkp control frames can be NULL terminated. 2023-11-24 13:15:22 +11:00
9e870858da Packet filenames can be in uppercase hex too 2023-11-24 12:18:19 +11:00
0800c48928 Use regex for received file evaluation. Fixes recording received packet names 2023-11-23 23:17:13 +11:00
455fed52ee Touch all our test files so that testing works when comparing mtimes - for TIC testing 2023-11-23 22:10:04 +11:00
19338edcb6 Optimise the queries used to display packet contents and show DBID if msgid is not available 2023-11-23 21:55:39 +11:00
b3dfca5b89 Optimise BINKP msg processing by using ltrim instead of skip_blanks. Should also address taurus mailers that add a NULL to the end of ADR messages 2023-11-23 19:11:14 +11:00
76dc90ceb3 Fixes to TIC processing that uses the Address::ftn_regex 2023-11-23 13:17:02 +11:00
a13028808a Optimise our address FTN regex 2023-11-23 12:22:39 +11:00
e5de4970d1 When originating a session, send anything received via the queue 2023-11-23 08:31:24 +11:00
7847728e52 Remove autohold on successful poll 2023-11-22 18:15:06 +11:00
b8670a5593 Change our TIC mtime comparision to compare with the actual mtime of the TIC file 2023-11-22 17:25:48 +11:00
fbcbe2c5a8 Address part of packet names is in hex 2023-11-22 16:49:14 +11:00
5f11f81be3 Fixes for TICs for nodelists 2023-11-22 16:41:14 +11:00
fcc2c23894 Our testing methods need to assert something 2023-11-22 15:58:00 +11:00
9fd8264c3f Rework TIC processing and added test cases 2023-11-22 15:58:00 +11:00
5b24ff944f Change System::match to return a single item regardless of role 2023-11-22 13:59:19 +11:00
3221d7f679 Show packet info when viewing echomail 2023-11-22 13:59:19 +11:00
f639e3ffab New attempt to making sure echomails have origin and senders path/seenby details (rework of #45d7823) 2023-11-22 13:59:19 +11:00
e8f4bf93bd Add a dontqueue option to packet::process 2023-11-22 13:59:19 +11:00
116f726885 Fix helpers checking for wtime existance 2023-11-22 10:40:28 +11:00
a74c5d5f5c Framework update 2023-11-22 10:40:28 +11:00
3a0847f13a For DNS records that we resolve for, return nodata if we dont serve that data type 2023-11-22 10:40:28 +11:00
509cdd7ea6 Fix incorrect subjects on some notifications 2023-11-22 10:40:28 +11:00
45d78233b2 Disabling adding to path - its adding to the end of the path which is not correct 2023-11-22 10:40:28 +11:00
4a870b6587 Improvements to echomail path handling, ensuring sender and pktsrc are in the path 2023-11-22 10:40:28 +11:00
67747c062a Add mtime to receiving filename, so that we dont have name clashes with systems that use the same archive name for our host 2023-11-17 16:30:19 +11:00
250e584c03 Routed Netmails should still have the netmail dest address, not the hub's address 2023-11-15 22:56:26 +11:00
7087fe9bbb Throw an exception when we cannot determine the end of the message/packet 2023-11-15 22:12:09 +11:00
3b99c409e0 When a netmail has a msgid, dont overwrite it 2023-11-15 11:19:14 +11:00
ea42a347eb As per RFC 2308, add SOA to authoriative answers with nodata, or errors 2023-11-13 07:57:01 +11:00
85243d128e Fixes for TIC processing and not identifying path/seenby correctly 2023-10-29 21:28:29 +11:00
7d82cbcf12 Since we know the zone, we know the domain name when parsing addresses 2023-10-26 11:14:54 +11:00
a886a389a8 Fix for echomail notifications, when echomails come from a point. Auto detect address when manually processing packets. 2023-10-26 11:02:36 +11:00
f9d24db9f8 Minor changes to optimise new installs 2023-10-18 20:03:23 +11:00
8ce3ce8164 Implement multiarch docker build and enable armv7l 2023-10-16 21:51:44 +11:00
a7e8cc7568 Implement HAPROXY proto support 2023-10-13 08:57:22 +11:00
c8ab8d3db3 Exported echomail should have the Hub as the OrigNet/OrigNode 2023-10-09 21:54:46 +11:00
953d3725b2 Another enhancement to the linking system 2023-10-09 17:48:26 +11:00
8332f485d1 Framework update 2023-10-09 17:47:45 +11:00
b32020e60f Nodelist import debugging to make sure we do select nodelists correctly 2023-10-07 21:09:00 +11:00
2c504c3d66 Include receive timestamp in packet:info and now using Storage::disk to find file 2023-10-07 21:09:00 +11:00
df5cc8c2d4 TIC processing fixes, was unable to find TIC file 2023-10-07 21:09:00 +11:00
4616feacda Fix some redirects now that self::class,'home' is no longer defined 2023-10-07 21:09:00 +11:00
7a9b6d5015 Change network/ to domain/view/ 2023-10-07 21:09:00 +11:00
654e7bd2aa Remove remaining ftn/ paths, no functional changes 2023-10-07 21:09:00 +11:00
b25e6f432c Rework DomainController/UserController methods and paths, no functional changes 2023-10-07 21:09:00 +11:00
27985dbf0b Rework ZoneController methods and paths, no functional changes 2023-10-07 21:09:00 +11:00
fda68bba04 Rework SystemController methods and paths, no functional changes 2023-10-07 21:09:00 +11:00
c86d8d8952 Logging to catch some hex2bin exceptions with 2/109 2023-10-06 22:52:03 +11:00
614d332fae Add ifcico to DNS query responses 2023-10-06 22:52:03 +11:00
0cabdcd3c1 Still return TXT records even if a system doesnt have an address 2023-10-06 22:52:03 +11:00
495a27cfed Enhance the system link/register selection 2023-10-06 22:52:03 +11:00
32c0088339 Rework nodelist import and ignoring addresses that we manage 2023-10-06 22:52:03 +11:00
b854cf9fe0 Better catch TIC file exceptions, enable moving TIC files if fido.packet_keep is defined 2023-10-04 22:22:01 +11:00
ce7a96ca2a Logging cosmetic changes only in Zmodem 2023-10-04 22:06:16 +11:00
28e30a05e6 Make passwords mandatory 2023-10-04 16:26:05 +11:00
e75be34afd Detach users when deleting a system 2023-10-04 15:58:46 +11:00
d82f8ac8b3 Catch bad DNS queries and reduce exception logging 2023-10-04 15:50:24 +11:00
0fcb628c11 Non functional cosmetic updates 2023-10-04 15:49:44 +11:00
c7e707c143 Attempt to catch errors creating address for dovenet mail 2023-10-04 12:17:16 +11:00
62f0c1a909 DNS server now responds to SRV and TXT records 2023-10-03 23:15:21 +11:00
073d95f605 Reduce the exception noise with queries that we dont parse correctly 2023-10-03 20:58:23 +11:00
2a50a1d795 When we dont have session() details return a blank string instead of null 2023-10-03 09:17:30 +11:00
782acad560 When processing packets on the command line, send it to the queue 2023-09-27 11:19:36 +10:00
c0c8861c08 Fix for Serialization of 'finfo' is not allowed 2023-09-24 00:01:44 +10:00
d11a2a5b8d Update nodelist import to exclude systems managed by us 2023-09-23 23:15:42 +10:00
ff04de52b5 Rework TIC processing to use Storage::disk(). Implemented handling of replaces and files that already exist 2023-09-23 22:40:17 +10:00
56544b89e1 Change background for graphics to #000000 2023-09-22 21:17:00 +10:00
2ae24b9955 Move fido configuation items into fido namespace. If keeping packets move them into a date aligned subdir 2023-09-22 15:35:08 +10:00
22c8b3df74 Respond to areafix netmails 2023-09-21 15:25:18 +10:00
2e7aecff57 Show icon to indicate address valid or not 2023-09-20 23:03:17 +10:00
b7c1c97cf7 Catch DNS Query that fail unpack() 2023-09-20 22:26:35 +10:00
612efda945 Process packet seenby/path/via lines when saving echomail/netmail 2023-09-20 21:37:18 +10:00
7fedf88d8c Hopefully a fix to stop clrghouz creating systems called Discovered System 2023-09-19 22:16:25 +10:00
11f9adf11a Fix seenby sort order, with flatten domains 2023-09-19 17:28:25 +10:00
eebe8a159d Fix address reported when scheduling a poll 2023-09-19 13:54:35 +10:00
3a35bce9e7 Changing System::match() to not include NC, some debugging updates 2023-09-19 11:29:08 +10:00
5e67be5ba1 Alert message for echomails with security violations is using wrong address 2023-09-19 11:29:08 +10:00
f315c71ca9 Fix path to public/logo 2023-09-19 11:29:08 +10:00
4343774079 Dont abort a session when there is an invalid FTN presented 2023-09-19 11:29:08 +10:00
eb40f94e37 Fix for binkd when remote present binkp protocol in brackets 2023-09-19 11:29:08 +10:00
cc04ddd7b3 More work to ensure messages from a node are valid for the domain, and fix domain flatten to check for zone if one is supplied 2023-09-17 15:54:47 +10:00
e611dcbe11 Filter available echoareas/fileareas based on security 2023-09-17 00:14:46 +10:00
073fa466d6 Added mail:list 2023-09-16 22:12:19 +10:00
708d9a9f67 More work to decommission rogue_path 2023-09-16 21:39:34 +10:00
c1d6d48a3c Dont enable rogue_path - it looses our true path for messages - instead create addresses in the path we dont know about. 2023-09-15 16:59:46 +10:00
6e133770fc An enhancement to ensure that flatten domains gets the correct FTN 2023-09-15 15:20:19 +10:00
a991db788e For AddressPoll, force using our file cache - seems sometimes the key doesnt release with memcached 2023-09-15 14:28:07 +10:00
096e37ef35 Removed packet cache, it wasnt used and not needed since we can queue large packets. Renamed to for consistent variable when using Packet::process() 2023-09-15 08:14:27 +10:00
2f878b6e64 Added filearea import 2023-09-14 23:42:25 +10:00
ec5c28a03e Added ignore_crc option to nodelist import 2023-09-14 23:06:02 +10:00
ff8c370d86 Move packet processing into a job 2023-09-13 20:58:22 +10:00
314 changed files with 22550 additions and 7997 deletions

View File

@ -2,7 +2,8 @@ APP_NAME="Clearing Houz"
APP_ENV=production APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=false APP_DEBUG=false
APP_URL=http://localhost APP_URL=http://clrghouz
APP_TIMEZONE=
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOG_LEVEL=info LOG_LEVEL=info
@ -10,8 +11,8 @@ LOG_LEVEL=info
DB_CONNECTION=pgsql DB_CONNECTION=pgsql
DB_HOST=postgres DB_HOST=postgres
DB_PORT=5432 DB_PORT=5432
DB_DATABASE=laravel DB_DATABASE=clrghouz
DB_USERNAME=laravel DB_USERNAME=clrghouz
DB_PASSWORD= DB_PASSWORD=
#DB_SSLMODE=prefer #DB_SSLMODE=prefer
#DB_SSLROOTCERT=/var/www/html/config/ssl/ca.crt #DB_SSLROOTCERT=/var/www/html/config/ssl/ca.crt
@ -19,6 +20,7 @@ DB_PASSWORD=
#DB_SSLKEY=/var/www/html/config/ssl/client.key #DB_SSLKEY=/var/www/html/config/ssl/client.key
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
MEMCACHED_HOST=memcached
CACHE_DRIVER=memcached CACHE_DRIVER=memcached
QUEUE_CONNECTION=database QUEUE_CONNECTION=database
SESSION_DRIVER=file SESSION_DRIVER=file
@ -46,6 +48,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
FIDO_DIR=fido FIDO_DIR=fido
FIDO_PACKET_KEEP= FIDO_PACKET_KEEP=
FIDO_STRICT=false
FILESYSTEM_DISK=s3 FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=

View File

@ -2,7 +2,7 @@ APP_NAME="Clearing Houz Testing"
APP_ENV=testing APP_ENV=testing
APP_KEY=base64:FiMSvv4J7jDfy6W/sHrQ9YImuUYaxynYCcXQJwp/6Tc= APP_KEY=base64:FiMSvv4J7jDfy6W/sHrQ9YImuUYaxynYCcXQJwp/6Tc=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://clrghouz
APP_TIMEZONE=Australia/Melbourne APP_TIMEZONE=Australia/Melbourne
LOG_CHANNEL=stderr LOG_CHANNEL=stderr
@ -41,7 +41,8 @@ PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
FIDO_DIR=fido FIDO_DIR=test
FIDO_DIR_FILES=local
FIDO_PACKET_KEEP=true FIDO_PACKET_KEEP=true
FILESYSTEM_DISK=s3 FILESYSTEM_DISK=s3

View File

@ -0,0 +1,147 @@
name: Create Docker Image
run-name: ${{ gitea.actor }} Building Docker Image 🐳
on: [push]
env:
VERSION: latest
DOCKER_HOST: tcp://127.0.0.1:2375
jobs:
# test:
# strategy:
# matrix:
# arch:
# - x86_64
# # arm64
#
# name: Test Application
# runs-on: docker-${{ matrix.arch }}
# container:
# image: gitea.dege.au/docker/php:8.3-fpm-pgsql-server-test
#
# steps:
# - name: Environment Setup
# run: |
# # If we have a proxy use it
# if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
# # Some pre-reqs
# apk add git nodejs
# ## Some debugging info
# # env|sort
#
# - name: Code Checkout
# uses: actions/checkout@v4
#
# - name: Run Tests
# run: |
# mv .env.testing .env
# # Install Composer and project dependencies.
# mkdir -p ${COMPOSER_HOME}
# if [ -n "${{ secrets.COMPOSER_GITHUB_TOKEN }}" ]; then echo ${{ secrets.COMPOSER_GITHUB_TOKEN }} > ${COMPOSER_HOME}/auth.json; fi
# composer install
# # Generate an application key. Re-cache.
# php artisan key:generate
# php artisan migrate
# php artisan db:seed
# # run laravel tests
# touch storage/app/test/*ZIP storage/app/test/file/*
# XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text --colors=never
build:
strategy:
matrix:
arch:
- x86_64
- arm64
# needs: [test]
name: Build Docker Image
runs-on: docker-${{ matrix.arch }}
container:
image: docker:dind
privileged: true
env:
ARCH: ${{ matrix.arch }}
VERSIONARCH: ${{ env.VERSION }}-${{ env.ARCH }}
steps:
- name: Environment Setup
run: |
# If we have a proxy use it
if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
# Some pre-reqs
apk add git curl nodejs
# Start docker
( dockerd --host=tcp://0.0.0.0:2375 --tls=false & ) && sleep 3
## Some debugging info
# docker info && docker version
env|sort
echo "PRT: ${{ secrets.PKG_WRITE_TOKEN }}"
- name: Registry FQDN Setup
id: registry
run: |
registry=${{ github.server_url }}
echo "registry=${registry##http*://}" >> "$GITHUB_OUTPUT"
- name: Container Registry Login
uses: docker/login-action@v2
with:
registry: ${{ steps.registry.outputs.registry }}
username: ${{ gitea.actor }}
password: ${{ secrets.PKG_WRITE_TOKEN }}
- name: Code Checkout
uses: actions/checkout@v4
- name: Record version
run: |
pwd
ls -al
echo ${GITHUB_SHA::8} > VERSION
cat VERSION
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
tags: "${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSIONARCH }}"
manifest:
name: Final Docker Image Manifest
runs-on: docker-x86_64
container:
image: docker:dind
privileged: true
needs: [build]
steps:
- name: Environment Setup
run: |
# If we have a proxy use it
if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
# Some pre-reqs
apk add git curl nodejs
# Start docker
( dockerd --host=tcp://0.0.0.0:2375 --tls=false & ) && sleep 3
- name: Registry FQDN Setup
id: registry
run: |
registry=${{ github.server_url }}
echo "registry=${registry##http*://}" >> "$GITHUB_OUTPUT"
- name: Container Registry Login
uses: docker/login-action@v2
with:
registry: ${{ steps.registry.outputs.registry }}
username: ${{ gitea.actor }}
password: ${{ secrets.PKG_WRITE_TOKEN }}
- name: Build Docker Manifest
run: |
docker manifest create ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }} \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-x86_64 \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-arm64
docker manifest push --purge ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}

View File

@ -1,14 +0,0 @@
stages:
- test
- build
# This folder is cached between builds
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache:
key: ${CI_JOB_NAME_SLUG}-${CI_COMMIT_REF_SLUG}
paths:
- vendor/
include:
- .gitlab-test.yml
- .gitlab-docker-x86_64.yml

View File

@ -1,27 +0,0 @@
docker:
variables:
VERSION: latest
DOCKER_HOST: tcp://docker:2375
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker info && docker version
- echo "$CI_JOB_TOKEN" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
- if [ -n "$GITHUB_TOKEN" ]; then cat $GITHUB_TOKEN |base64 -d > auth.json; fi
script:
- if [ -f init ]; then chmod 500 init; fi
- echo -n ${CI_COMMIT_SHORT_SHA} > VERSION
- rm -rf vendor/ database/schema database/seeders database/factories/*
- docker build -t ${CI_REGISTRY_IMAGE}:${VERSION} .
- docker push ${CI_REGISTRY_IMAGE}:${VERSION}
tags:
- docker
- x86_64
only:
- master

View File

@ -1,42 +0,0 @@
test:
image: ${CI_REGISTRY}/leenooks/php:8.1-fpm-alpine-pgsql-server-test
stage: test
# NOTE: This service is dependant on project file configuration, which is not there if the cache was deleted
# resulting in the testing to fail on the first run.
services:
- name: postgres:15-alpine
alias: postgres-test
variables:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
tags:
- php
only:
- master
before_script:
- mv .env.testing .env
# Install Composer and project dependencies.
- mkdir -p ${COMPOSER_HOME}
- if [ -n "$GITHUB_TOKEN" ]; then cat $GITHUB_TOKEN |base64 -d > ${COMPOSER_HOME}/auth.json; fi
- composer install
# Generate an application key. Re-cache.
- php artisan key:generate
- php artisan migrate
- php artisan db:seed
script:
# run laravel tests
- XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text --colors=never
# run frontend tests
# if you have any task for testing frontend
# set it in your package.json script
# comment this out if you don't have a frontend test
# npm test

View File

@ -28,7 +28,13 @@ class CompressedString implements CastsAttributes
? stream_get_contents($value) ? stream_get_contents($value)
: $value; : $value;
// If we get an error decompressing, it might not be zstd (or its already been done)
try {
return $value ? zstd_uncompress(base64_decode($value)) : ''; return $value ? zstd_uncompress(base64_decode($value)) : '';
} catch (\ErrorException $e) {
return $value;
}
} }
/** /**

101
app/Classes/BBS/Control.php Normal file
View File

@ -0,0 +1,101 @@
<?php
namespace App\Classes\BBS;
use App\Classes\BBS\Control\EditFrame;
use App\Classes\BBS\Control\Register;
use App\Classes\BBS\Control\Telnet;
abstract class Control
{
const prefix = 'App\Classes\Control\\';
// Has this control class finished with input
protected bool $complete = FALSE;
// The server object that is running this control class
protected Server $so;
/**
* What is the state of the server outside of this control.
* Should only contain
* + mode = Mode to follow outside of the control method
* + action = Action to run after leaving the control method
*
* @var array
*/
public array $state = [];
abstract public function handle(string $read): string;
public static function factory(string $name,Server $so,array $args=[])
{
switch ($name) {
case 'editframe':
return new EditFrame($so,$args);
case 'register':
return new Register($so);
case 'telnet':
return new Telnet($so);
default:
$c = (class_exists($name)) ? $name : self::prefix.$name;
$o = class_exists($c) ? new $c($so,$args) : NULL;
$so->log('debug',sprintf(($o ? 'Executing: %s' : 'Class doesnt exist: %s'),$c));
return $o;
}
}
public function __construct(Server $so,array $args=[])
{
$this->so = $so;
// Boot control, preparing anything before keyboard entry
$this->boot();
$this->so->log('info',sprintf('Initialised control %s',get_class($this)));
}
public function __get(string $key): mixed
{
switch ($key) {
case 'complete':
return $this->complete;
case 'name':
return get_class($this);
default:
throw new \Exception(sprintf('%s:! Unknown key: %s',static::LOGKEY,$key));
}
}
// Default boot method if a child class doesnt have one.
protected function boot()
{
$this->state['mode'] = FALSE;
}
/**
* Has control completed?
* @deprecated use $this->complete;
*/
public function complete()
{
return $this->complete;
}
/**
* If completing an Action frame, this will be called to submit the data.
*
* Ideally this should be overridden in a child class.
*/
public function process()
{
$this->complete = TRUE;
}
}

View File

@ -0,0 +1,198 @@
<?php
namespace App\Classes\BBS\Control;
use Illuminate\Support\Arr;
use App\Classes\BBS\Control;
use App\Classes\BBS\Frame;
use App\Classes\BBS\Server;
/**
* Class Edit Frame handles frame editing
*
* @package App\Classes\Control
*/
class EditFrame extends Control
{
private $x = 1;
private $y = 1;
// The frame applicable for this control (not the current rendered frame, thats in $so)
protected $fo = NULL;
public function __construct(Server $so,array $args=[])
{
if (! $args OR ! Arr::get($args,'fo') OR (! $args['fo'] instanceof Frame))
throw new \Exception('Missing frame to Edit');
$this->fo = $args['fo'];
parent::__construct($so);
}
protected function boot()
{
// Clear screen and setup edit.
$this->so->co->send(CLS.HOME.DOWN.CON);
// @todo Add page number + "EDIT" (prob only required for login pages which dont show page num)
$this->so->co->send($this->fo->raw().$this->so->moveCursor(1,2));
$this->updateBaseline();
}
public function handle(string $read): string
{
static $esc = FALSE;
static $brace = FALSE;
static $out = '';
static $key = '';
$out .= $read;
switch ($read)
{
case 'A':
if ($esc AND $brace)
{
$this->y--;
if ($this->y < 1) {
$this->y = 1;
$out = '';
}
$brace = $esc = FALSE;
}
break;
case 'B':
if ($esc AND $brace)
{
$this->y++;
if ($this->y > $this->fo->frame_length()) {
$this->y = $this->fo->frame_length();
$out = '';
}
$brace =$esc = FALSE;
}
break;
case 'C':
if ($esc AND $brace)
{
$this->x++;
if ($this->x > $this->fo->frame_width()) {
$this->x = $this->fo->frame_width();
$out = '';
}
$brace =$esc = FALSE;
}
break;
case 'D':
if ($esc AND $brace)
{
$this->x--;
if ($this->x < 1) {
$this->x = 1;
$out = '';
}
$brace = $esc = FALSE;
}
break;
case '[':
if ($esc)
$brace = TRUE;
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '0':
if ($esc AND $brace) {
$key .= $read;
} else {
$this->x++;
}
break;
case '~':
if ($esc AND $brace)
{
switch ($key)
{
// F9 Pressed
case 20:
break;
// F10 Pressed
case 21:
$this->complete = TRUE;
$this->state = ['action'=>ACTION_GOTO,'mode'=>NULL];
break;
}
$brace = $esc = FALSE;
$key = '';
}
break;
case ESC;
$esc = TRUE;
break;
case LF: $this->y++; break;
case CR; $this->x = 1; break;
default:
if ($esc)
$esc = FALSE;
$this->x++;
}
if (! $esc)
{
printf(" . SENDING OUT: %s\n",$out);
$this->so->co->send($out);
$this->updateBaseline();
$out = '';
}
printf(" . X:%d,Y:%d,C:%s,ESC:%s\n",
$this->x,
$this->y,
(ord($read) < 32 ? '.' : $read),
($esc AND $brace) ? 'TRUE' : 'FALSE');
return $read;
}
public function updateBaseline()
{
$this->so->sendBaseline(
$this->so->co,
sprintf('%02.0f:%02.0f]%s'.RESET.'[',
$this->y,
$this->x,
($this->fo->attr($this->x,$this->y) != '-' ? ESC.'['.$this->fo->attr($this->x,$this->y) : '').$this->fo->char($this->x,$this->y),
)
);
}
public function process()
{
dump(__METHOD__);
}
}

View File

@ -0,0 +1,158 @@
<?php
namespace App\Classes\BBS\Control;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use App\Classes\BBS\Control;
use App\Mail\SendToken;
use App\Models\User;
/**
* Class Register handles registration
*
* @todo REMOVE the force .WHITE at the end of each sendBaseline()
* @package App\Classes\Control
*/
class Register extends Control
{
private $data = [];
protected function boot()
{
$this->so->sendBaseline($this->so->co,GREEN.'Select User Name'.WHITE);
}
/**
* Handle Registration Form Input
*
* This function assumes the form has 7 fields in a specific order.
*
* @todo Make this form more dynamic, or put some configuration in a config file, so that there is flexibility
* in field placement.
* @param string $read
* @param array $current
* @return string
*/
public function handle(string $read,array $current=[]): string
{
// Ignore LF (as a result of pressing ENTER)
if ($read == LF)
return '';
// If we got a # we'll be completing field input.
if ($read == HASH OR $read == CR) {
// Does our field have data...
if ($x=$this->so->fo->getFieldCurrentInput()) {
switch ($this->so->fo->getFieldId()) {
// Username
case 0:
// See if the requested username already exists
if (User::where('login',$x)->exists()) {
$this->so->sendBaseline($this->so->co,RED.'USER ALREADY EXISTS'.WHITE);
return '';
}
$this->so->sendBaseline($this->so->co,GREEN.'Enter Real Name'.WHITE);
break;
// Real Name
case 1:
//$this->data['name'] = $x;
$this->so->sendBaseline($this->so->co,GREEN.'Enter Email Address'.WHITE);
break;
// Email Address
case 2:
if (Validator::make(['email'=>$x],[
'email'=>'email',
])->fails()) {
$this->so->sendBaseline($this->so->co,RED.'INVALID EMAIL ADDRESS'.WHITE);
return '';
};
// See if the requested email already exists
if (User::where('email',$x)->exists()) {
$this->so->sendBaseline($this->so->co,RED.'USER ALREADY EXISTS'.WHITE);
return '';
}
$this->data['email'] = $x;
$this->data['token'] = sprintf('%06.0f',rand(0,999999));
$this->so->sendBaseline($this->so->co,YELLOW.'PROCESSING...'.WHITE);
Mail::to($this->data['email'])->sendNow(new SendToken($this->data['token']));
if (Mail::failures()) {
dump('Failure?');
dump(Mail::failures());
}
$this->so->sendBaseline($this->so->co,GREEN.'Enter Password'.WHITE);
break;
// Enter Password
case 3:
$this->data['password'] = $x;
$this->so->sendBaseline($this->so->co,GREEN.'Confirm Password'.WHITE);
break;
// Confirm Password
case 4:
if ($this->data['password'] !== $x) {
$this->so->sendBaseline($this->so->co,RED.'PASSWORD DOESNT MATCH, *09 TO START AGAIN'.WHITE);
return '';
}
$this->so->sendBaseline($this->so->co,GREEN.'Enter Location'.WHITE);
break;
// Enter Location
case 5:
$this->so->sendBaseline($this->so->co,GREEN.'Enter TOKEN emailed to you'.WHITE);
break;
// Enter Token
case 6:
if ($this->data['token'] !== $x) {
$this->so->sendBaseline($this->so->co,RED.'TOKEN DOESNT MATCH, *09 TO START AGAIN'.WHITE);
return '';
}
$this->complete = TRUE;
break;
default:
$this->so->sendBaseline($this->so->co,RED.'HUH?');
}
} else {
// If we are MODE_BL, we need to return the HASH, otherwise nothing.
if (in_array($this->state['mode'],[MODE_BL,MODE_SUBMITRF,MODE_RFNOTSENT])) {
return $read;
} else {
$this->so->sendBaseline($this->so->co,RED.'FIELD REQUIRED...'.WHITE);
return '';
}
}
}
return $read;
}
}

View File

@ -0,0 +1,199 @@
<?php
namespace App\Classes\BBS\Control;
use App\Classes\BBS\Control;
/**
* Class Telnet
*
* This class looks after any telnet session commands
*
* TELNET http://pcmicro.com/netfoss/telnet.html
*
* @package App\Classes\Control
*/
final class Telnet extends Control
{
protected const LOGKEY = 'CT-';
/** @var int Data Byte */
public const TCP_IAC = 0xff;
/** @var int Indicates the demand that the other party stop performing, or confirmation that you are no
longer expecting the other party to perform, the indicated option */
public const TCP_DONT = 0xfe;
/** @var int Indicates the request that the other party perform, or confirmation that you are expecting
the other party to perform, the indicated option. */
public const TCP_DO = 0xfd;
/** @var int Indicates the refusal to perform, or continue performing, the indicated option. */
public const TCP_WONT = 0xfc;
/** @var int Indicates the desire to begin performing, or confirmation that you are now performing, the indicated option. */
public const TCP_WILL = 0xfb;
/** @var int Indicates that what follows is sub-negotiation of the indicated option. */
public const TCP_SB = 0xfa;
/** @var int The GA signal. */
public const TCP_GA = 0xf9;
/** @var int Erase Line. */
public const TCP_EL = 0xf8;
/** @var int Erase character. */
public const TCP_EC = 0xf7;
/** @var int Are you there? */
public const TCP_AYT = 0xf6;
/** @var int About output */
public const TCP_AO = 0xf5;
/** @var int Interrupt Process. */
public const TCP_IP = 0xf4;
/** @var int Break. */
public const TCP_BREAK = 0xf3;
/** @var int The data stream portion of a Synch. This should always be accompanied by a TCP Urgent notification. */
public const TCP_DM = 0xf2;
/** @var int No operation. */
public const TCP_NOPT = 0xf1;
/** @var int End of sub-negotiation parameters. */
public const TCP_SE = 0xf0;
public const TCP_BINARY = 0x00;
public const TCP_OPT_ECHO = 0x01;
public const TCP_OPT_SUP_GOAHEAD = 0x03;
public const TCP_OPT_TERMTYPE = 0x18;
public const TCP_OPT_WINDOWSIZE = 0x1f;
public const TCP_OPT_LINEMODE = 0x22;
private bool $option = FALSE;
private string $note;
private string $terminal = '';
public static function send_iac($key): string
{
$send = chr(self::TCP_IAC);
switch ($key) {
case 'are_you_there':
$send .= chr(self::TCP_AYT);
break;
case 'do_echo':
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_ECHO);
break;
case 'dont_echo':
$send .= chr(self::TCP_DONT).chr(self::TCP_OPT_ECHO);
break;
case 'will_echo':
$send .= chr(self::TCP_WILL).chr(self::TCP_OPT_ECHO);
break;
case 'wont_echo':
$send .= chr(self::TCP_WONT).chr(self::TCP_OPT_ECHO);
break;
case 'do_opt_termtype':
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_TERMTYPE);
break;
case 'do_suppress_goahead':
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_SUP_GOAHEAD);
break;
case 'sn_end':
$send .= chr(self::TCP_SE);
break;
case 'sn_start':
$send .= chr(self::TCP_SB);
break;
case 'wont_linemode':
$send .= chr(self::TCP_WONT).chr(self::TCP_OPT_LINEMODE);
break;
case 'will_xmit_binary':
$send .= chr(self::TCP_WILL).chr(self::TCP_BINARY);
break;
default:
throw new \Exception(sprintf('%s:! Unknown key: %s',$key));
}
return $send;
}
public function handle(string $read): string
{
$this->so->log('debug',sprintf('%s:+ Session Char [%02x] (%c)',self::LOGKEY,ord($read),$read),['complete'=>$this->complete,'option'=>$this->option]);
switch (ord($read)) {
// Command being sent.
case self::TCP_IAC:
$this->complete = FALSE;
$this->note = 'IAC ';
break;
case self::TCP_SB:
$this->option = TRUE;
break;
case self::TCP_SE:
$this->option = FALSE;
$this->complete = TRUE;
$this->so->log('debug',sprintf('%s:%% Session Terminal: %s',self::LOGKEY,$this->terminal));
break;
case self::TCP_DO:
$this->note .= 'DO ';
break;
case self::TCP_WILL:
$this->note .= 'WILL ';
break;
case self::TCP_WONT:
$this->note .= 'WONT ';
break;
case self::TCP_OPT_TERMTYPE:
break;
case self::TCP_OPT_ECHO:
$this->note .= 'ECHO';
$this->complete = TRUE;
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
break;
case self::TCP_OPT_SUP_GOAHEAD:
$this->note .= 'SUPPRESS GO AHEAD';
$this->complete = TRUE;
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
break;
case self::TCP_OPT_WINDOWSIZE:
$this->note .= 'WINDOWSIZE';
$this->complete = TRUE;
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
break;
default:
if ($this->option && $read)
$this->terminal .= $read;
else
$this->so->log('debug',sprintf('%s:= Unhandled char in session_init: [%02x] (%c)',self::LOGKEY,ord($read),$read));
}
if ($this->complete)
$this->so->log('debug',sprintf('%s:= TELNET control COMPLETE',self::LOGKEY));
return '';
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Classes\BBS\Control;
use App\Classes\BBS\Control;
/**
* Class Test
*
* This is a test class for Control Validation Processing
*
* @package App\Classes\Control
*/
class Test extends Control
{
public function boot()
{
$this->so->co->send(CLS.HOME.DOWN.CON);
$this->so->co->send('Press 1, or 2, or 4, 0 to end.');
}
// @todo *00/09 doesnt work
public function handle(string $read): string
{
switch ($read)
{
case 0:
$this->complete = TRUE;
$read = '';
break;
case 1:
$this->so->co->send('You pressed ONE.');
$read = '';
break;
case 2:
$this->so->co->send('You pressed TWO.');
$read = '';
break;
case 3:
$this->so->co->send('You pressed THREE.');
$read = '';
break;
case 4:
$this->so->co->send('You pressed FOUR.');
$read = '';
break;
}
return $read;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Classes\BBS\Exceptions;
use Exception;
class ActionMissingInputsException extends Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Classes\BBS\Exceptions;
use Exception;
class InvalidPasswordException extends Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Classes\BBS\Exceptions;
use Exception;
class NoRouteException extends Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Classes\BBS\Exceptions;
use Exception;
class ParentNotFoundException extends Exception
{
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Classes\BBS\Frame;
use Illuminate\Support\Collection;
use App\Classes\BBS\Exceptions\ActionMissingInputsException;
use App\Classes\BBS\Frame\Action\{Login,Register};
use App\Classes\BBS\Server;
use App\Models\User;
abstract class Action
{
private Collection $fields_input;
protected User $uo;
public const actions = [
'login' => Login::class,
'register' => Register::class,
];
protected const fields = [];
abstract public function handle(): bool;
abstract public function preSubmitField(Server $server,Field $field): ?string;
public static function factory(string $class): self
{
if (array_key_exists($class,self::actions)) {
$class = self::actions[$class];
return new $class;
}
throw new \Exception(sprintf('Call to action [%s] doesnt have a class to execute',$class));
}
public function __get(string $key): mixed
{
switch ($key) {
case 'fields_input':
return $this->{$key};
default:
if (($x=$this->fields_input->search(function($item) use ($key) { return $item->name === $key; })) !== FALSE)
return $this->fields_input->get($x)->value;
else
return NULL;
}
}
public function __set(string $key,mixed $value): void
{
switch ($key) {
case 'fields_input':
$this->{$key} = $value;
break;
default:
throw new \Exception('Unknown key: '.$key);
}
}
public function init(): void
{
if (! isset($this->fields_input))
throw new \Exception(sprintf('Missing fields_input in [%s]',get_class($this)));
// First field data element is user, the second is the password
if (count($x=collect(static::fields)->diff($this->fields_input->pluck('name'))))
throw new ActionMissingInputsException(sprintf('Login missing %s',$x->join(',')));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Classes\BBS\Frame\Action;
use Illuminate\Support\Facades\Hash;
use App\Classes\BBS\Exceptions\{ActionMissingInputsException,InvalidPasswordException};
use App\Classes\BBS\Frame\{Action,Field};
use App\Classes\BBS\Server;
use App\Models\User;
class Login extends Action
{
protected const fields = ['USER','PASS'];
public function __get(string $key): mixed
{
switch ($key) {
case 'user': return $this->uo;
default:
return parent::__get($key);
}
}
/**
* Handle user logins
*
* @return bool
* @throws ActionMissingInputsException
* @throws InvalidPasswordException
*/
public function handle(): bool
{
parent::init();
$this->uo = User::where('name',$this->USER)->orWhere('alias',$this->USER)->firstOrFail();
if (! Hash::check($this->PASS,$this->uo->password))
throw new InvalidPasswordException(sprintf('Password doesnt match for [%s]',$this->USER));
return TRUE;
}
public function preSubmitField(Server $server,Field $field): ?string
{
// Noop
return NULL;
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace App\Classes\BBS\Frame\Action;
use Carbon\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use App\Classes\BBS\Frame\{Action,Field};
use App\Classes\BBS\Exceptions\ActionMissingInputsException;
use App\Classes\BBS\Server;
use App\Mail\BBS\SendToken;
use App\Models\User;
/**
* Class Register
* This handles the data received for account registration
*
* @package App\Classes\Frame\Action
*/
class Register extends Action
{
protected const fields = ['EMAIL','USER','PASS','FULLNAME','TOKEN'];
private string $token = '';
public function __get(string $key): mixed
{
switch ($key) {
case 'user': return $this->uo;
default:
return parent::__get($key);
}
}
/**
* Handle user logins
*
* @return bool
* @throws ActionMissingInputsException
*/
public function handle(): bool
{
parent::init();
$this->uo = new User;
$this->uo->name = $this->fields_input->where('name','FULLNAME')->first()->value;
$this->uo->email = $this->fields_input->where('name','EMAIL')->first()->value;
$this->uo->email_verified_at = Carbon::now();
$this->uo->password = Hash::make($x=$this->fields_input->where('name','PASS')->first()->value);
$this->uo->active = TRUE;
$this->uo->last_on = Carbon::now();
$this->uo->alias = $this->fields_input->where('name','USER')->first()->value;
$this->uo->save();
return TRUE;
}
public function preSubmitField(Server $server,Field $field): ?string
{
switch ($field->name) {
// Send a token
case 'EMAIL':
// Make sure we got an email address
if (Validator::make(['email'=>$field->value],[
'email'=>'email',
])->fails()) {
return 'INVALID EMAIL ADDRESS';
}
// See if the requested email already exists
if (User::where('email',$field->value)->exists())
return 'USER ALREADY EXISTS';
Log::info(sprintf('Sending token to [%s]',$field->value));
$server->sendBaseline(RED.'SENDING TOKEN...');
$this->token = sprintf('%06.0f',rand(0,999999));
$sent = Mail::to($field->value)->send(new SendToken($this->token));
$server->sendBaseline(RED.'SENT');
break;
case 'USER':
if (str_contains($field->value,' '))
return 'NO SPACES IN USER NAMES';
// See if the requested username already exists
if (User::where('alias',$field->value)->exists())
return 'USER ALREADY EXISTS';
// Clear the baseline from EMAIL entry
$server->sendBaseline('');
break;
case 'TOKEN':
if ($field->value !== $this->token)
return 'INVALID TOKEN';
break;
}
return NULL;
}
}

View File

@ -0,0 +1,290 @@
<?php
namespace App\Classes\BBS\Frame;
use App\Classes\BBS\Page\{Ansi,Viewdata};
use App\Models\BBS\Mode;
class Char {
/** @var int|null Attributes for the character (ie: color) */
private ?int $attr;
/** @var string|null Character to be shown */
private ?string $ch;
public function __construct(string $ch=NULL,int $attr=NULL)
{
$this->ch = $ch;
$this->attr = $attr;
}
public function __get(string $key): mixed
{
switch ($key) {
case 'attr': return $this->attr;
case 'ch': return $this->ch;
default:
throw new \Exception('Unknown key:'.$key);
}
}
public function __isset($key): bool
{
return isset($this->{$key});
}
public function __set(string $key,mixed $value): void
{
switch ($key) {
case 'ch':
if (strlen($value) !== 1)
throw new \Exception(sprintf('CH can only be 1 char: [%s]',$value));
$this->{$key} = $value;
break;
default:
throw new \Exception('Unknown key:'.$key);
}
}
public function __toString()
{
return sprintf('%04x [%s]|',$this->attr,$this->ch);
}
/**
* Return the color codes required to draw the current character
*
* @param Mode $mo Service we are rendering for
* @param int|null $last last rendered char
* @param bool $debug debug mode
* @return string|NULL
* @throws \Exception
*/
public function attr(Mode $mo,int $last=NULL,bool $debug=FALSE): string|NULL
{
$ansi = collect();
if ($debug)
dump('- last:'.$last.', this:'.$this->attr);
switch ($mo->name) {
case 'ansi':
if ($debug) {
dump(' - this BG_BLACK:'.($this->attr & Ansi::BG_BLACK));
dump(' - last BG_BLACK:'.($last & Ansi::BG_BLACK));
dump(' - this HIGH:'.($this->attr & Ansi::HIGH));
dump(' - last HIGH:'.($last & Ansi::HIGH));
dump(' - this BLINK:'.($this->attr & Ansi::BLINK));
dump(' - last BLINK:'.($last & Ansi::BLINK));
}
// If high was in the last, and we dont have high now, we need 0, but we need to turn back on flash if it was there
// If flash was in the last, and we dont have flash now, we need to 0 but we need to turn on high if it was there
$reset = FALSE;
if ((($this->attr & Ansi::BG_BLACK) && (! ($last & Ansi::BG_BLACK)))
|| ((! ($this->attr & Ansi::BLINK)) && ($last & Ansi::BLINK))
|| ((! ($this->attr & Ansi::HIGH)) && ($last & Ansi::HIGH)))
{
$ansi->push(Ansi::I_CLEAR_CODE);
$reset = TRUE;
$last = Ansi::BG_BLACK|Ansi::LIGHTGRAY;
}
if (($this->attr & Ansi::HIGH)
&& ((($this->attr & Ansi::HIGH) !== ($last & Ansi::HIGH)) || ($reset && ($last & Ansi::HIGH)))) {
$ansi->push(Ansi::I_HIGH_CODE);
}
if (($this->attr & Ansi::BLINK)
&& ((($this->attr & Ansi::BLINK) !== ($last & Ansi::BLINK)) || ($reset && ($last & Ansi::BLINK)))) {
$ansi->push(Ansi::I_BLINK_CODE);
}
$c = ($this->attr & 0x07);
$l = ($last & 0x07);
// Foreground
switch ($c) {
case Ansi::BLACK:
$r = Ansi::FG_BLACK_CODE;
break;
case Ansi::RED:
$r = Ansi::FG_RED_CODE;
break;
case Ansi::GREEN:
$r = Ansi::FG_GREEN_CODE;
break;
case Ansi::BROWN:
$r = Ansi::FG_BROWN_CODE;
break;
case Ansi::BLUE:
$r = Ansi::FG_BLUE_CODE;
break;
case Ansi::MAGENTA:
$r = Ansi::FG_MAGENTA_CODE;
break;
case Ansi::CYAN:
$r = Ansi::FG_CYAN_CODE;
break;
case Ansi::LIGHTGRAY:
$r = Ansi::FG_LIGHTGRAY_CODE;
break;
}
if ($r && ($c !== $l))
$ansi->push($r);
// Background
if ($this->attr & 0x70) {
$c = ($this->attr & 0x70);
$l = ($last & 0x70);
switch ($this->attr & 0x70) {
case Ansi::BG_BLACK:
$r = Ansi::BG_BLACK_CODE;
break;
case Ansi::BG_RED:
$r = Ansi::BG_RED_CODE;
break;
case Ansi::BG_GREEN:
$r = Ansi::BG_GREEN_CODE;
break;
case Ansi::BG_BROWN:
$r = Ansi::BG_BROWN_CODE;
break;
case Ansi::BG_BLUE:
$r = Ansi::BG_BLUE_CODE;
break;
case Ansi::BG_MAGENTA:
$r = Ansi::BG_MAGENTA_CODE;
break;
case Ansi::BG_CYAN:
$r = Ansi::BG_CYAN_CODE;
break;
case Ansi::BG_LIGHTGRAY:
$r = Ansi::BG_LIGHTGRAY_CODE;
break;
}
if ($r && ($c !== $l))
$ansi->push($r);
}
if ($debug)
dump([' - ansi:' =>$ansi]);
return $ansi->count() ? sprintf('%s[%sm',($debug ? '': "\x1b"),$ansi->join(';')) : NULL;
case 'viewdata':
if ($debug)
dump(sprintf('Last: %02x, Attr: %02x',$last,$this->attr));
switch ($this->attr) {
// \x08
case Viewdata::BLINK:
$r = Viewdata::I_BLINK_CODE;
break;
// \x09
case Viewdata::STEADY:
$r = Viewdata::I_STEADY;
break;
// \x0c
case Viewdata::NORMAL:
$r = Viewdata::I_NORMAL;
break;
// \x0d
case Viewdata::DOUBLE:
$r = Viewdata::I_DOUBLE_CODE;
break;
// \x18
case Viewdata::CONCEAL:
$r = Viewdata::I_CONCEAL;
break;
// \x19
case Viewdata::BLOCKS:
$r = Viewdata::I_BLOCKS;
break;
// \x1a
case Viewdata::SEPARATED:
$r = Viewdata::I_SEPARATED;
break;
// \x1c
case Viewdata::BLACKBACK:
$r = Viewdata::I_BLACKBACK;
break;
// \x1d
case Viewdata::NEWBACK:
$r = Viewdata::I_NEWBACK;
break;
// \x1e
case Viewdata::HOLD:
$r = Viewdata::I_HOLD;
break;
// \x1f
case Viewdata::RELEASE:
$r = Viewdata::I_REVEAL;
break;
// Not handled
// \x0a-b,\x0e-f,\x1b
case 0xff00:
dump($this->attr);
break;
default:
$mosiac = ($this->attr & Viewdata::MOSIAC);
$c = ($this->attr & 0x07);
if ($debug)
dump(sprintf('Last: %02x, Attr: %02x, Color: %02x',$last,$this->attr,$c));
// Color control \x00-\x07, \x10-\x17
switch ($c) {
/*
case Viewdata::BLACK:
$r = Viewdata::FG_BLACK_CODE;
break;
*/
case Viewdata::RED:
$r = $mosiac ? Viewdata::MOSIAC_RED_CODE : Viewdata::FG_RED_CODE;
break;
case Viewdata::GREEN:
$r = $mosiac ? Viewdata::MOSIAC_GREEN_CODE : Viewdata::FG_GREEN_CODE;
break;
case Viewdata::YELLOW:
$r = $mosiac ? Viewdata::MOSIAC_YELLOW_CODE : Viewdata::FG_YELLOW_CODE;
break;
case Viewdata::BLUE:
$r = $mosiac ? Viewdata::MOSIAC_BLUE_CODE : Viewdata::FG_BLUE_CODE;
break;
case Viewdata::MAGENTA:
$r = $mosiac ? Viewdata::MOSIAC_MAGENTA_CODE : Viewdata::FG_MAGENTA_CODE;
break;
case Viewdata::CYAN:
$r = $mosiac ? Viewdata::MOSIAC_CYAN_CODE : Viewdata::FG_CYAN_CODE;
break;
case Viewdata::WHITE:
$r = $mosiac ? Viewdata::MOSIAC_WHITE_CODE : Viewdata::FG_WHITE_CODE;
break;
default:
if ($debug)
dump('Not a color?:'.$c);
return NULL;
}
}
if ($debug)
dump(sprintf('= result: ESC[%s](%02x) for [%s]',chr($r),$r,$this->ch));
return chr($r);
default:
throw new \Exception($this->type.': has not been implemented');
}
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace App\Classes\BBS\Frame;
use Illuminate\Support\Arr;
final class Field
{
private array $attributes = [];
private const attributes = [
'attribute', // Color attribute when rendering values
'pad', // Pad character remaining characters up to length
'size', // Size of the field
'name', // Field name
'type', // Type of field
'value', // Current value
'x', // X position in the frame
'y', // Y position in the frame
];
/** @var string[] Attributes that should be masked */
private const mask = [
'p',
];
private const mask_attribute = '*';
public function __construct(array $values)
{
array_walk($values,function($value,$key) {
$this->{$key} = $value;
});
}
public function __get($key): mixed
{
switch ($key) {
case 'can_add':
return strlen($this->value) < $this->size;
case 'mask':
return in_array($this->type,self::mask) ? '*' : NULL;
case 'X':
return $this->x+strlen($this->value);
default:
return Arr::get($this->attributes,$key);
}
}
public function __isset($key): bool
{
return isset($this->attributes[$key]);
}
public function __set($key,$value): void
{
if (! in_array($key,self::attributes))
throw new \Exception('Unknown attribute key:'.$key);
$this->attributes[$key] = $value;
}
/**
* Append a char to the value, only if there is space to do so
*
* @param string $char
* @return bool
*/
public function append(string $char): bool
{
if (is_null($this->value))
$this->clear();
if ($this->can_add) {
$this->value .= $char;
return TRUE;
}
return FALSE;
}
/**
* Clear the field value
*
* @return void
*/
public function clear(): void
{
$this->value = '';
}
/**
* Delete a character from the value, only if there are chars to do so
*
* @return bool
*/
public function delete(): bool
{
if (strlen($this->value)) {
$this->value = substr($this->value,0,-1);
return TRUE;
}
return FALSE;
}
}

628
app/Classes/BBS/Page.php Normal file
View File

@ -0,0 +1,628 @@
<?php
namespace App\Classes\BBS;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use App\Classes\BBS\Exceptions\{NoRouteException,ParentNotFoundException};
use App\Classes\BBS\Frame\{Action,Field};
use App\Models\BBS\{Frame,Mode};
use App\Models\User;
/**
* The current page object
*
* @property page The full page number requested
*/
abstract class Page
{
/**
* Color attributes can fit in an int
* + Bit 0-2 off = Black foreground
* + Foreground colors bits (0-2)
* + High Intensity 1 bit (3)
* + Bit 4-6 off = Black background
* + Background colors bits (4-6)
* + Flash 1 bit (7)
*/
public const BLINK = 1<<7; /* blink bit */
public const HIGH = 1<<3; /* high intensity (bright) foreground bit */
/* foreground colors */
public const BLACK = 0; /* dark colors (HIGH bit unset) */
public const BLUE = 1;
public const GREEN = 2;
public const CYAN = 3;
public const RED = 4;
public const MAGENTA = 5;
public const BROWN = 6;
public const LIGHTGRAY = 7;
public const DARKGRAY = self::HIGH | self::BLACK; /* light colors (HIGH bit set) */
public const LIGHTBLUE = self::HIGH | self::BLUE;
public const LIGHTGREEN = self::HIGH | self::GREEN;
public const LIGHTCYAN = self::HIGH | self::CYAN;
public const LIGHTRED = self::HIGH | self::RED;
public const LIGHTMAGENTA = self::HIGH | self::MAGENTA;
public const YELLOW = self::HIGH | self::BROWN;
public const WHITE = self::HIGH | self::LIGHTGRAY;
public const BG_BLACK = 0x100; /* special value for ansi() */
public const BG_BLUE = (self::BLUE<<4);
public const BG_GREEN = (self::GREEN<<4);
public const BG_CYAN = (self::CYAN<<4);
public const BG_RED = (self::RED<<4);
public const BG_MAGENTA = (self::MAGENTA<<4);
public const BG_BROWN = (self::BROWN<<4);
public const BG_LIGHTGRAY = (self::LIGHTGRAY<<4);
public const FRAMETYPE_INFO = 'i';
public const FRAMETYPE_ACTION = 'a';
public const FRAMETYPE_RESPONSE = 'r';
public const FRAMETYPE_LOGIN = 'l';
public const FRAMETYPE_TERMINATE = 't';
public const FRAMETYPE_EXTERNAL = 'x';
private int $frame;
private string $index;
/** @var Mode Our BBS mode model object */
protected Mode $mo;
/** @var Frame|null Our frame model object */
private ?Frame $fo = NULL;
/** @var Collection Users page retrieval history */
private Collection $history;
/* Our window layout */
protected Window $layout;
private Window $content;
private Window $header;
private Window $provider;
private Window $pagenum;
private Window $unit;
private bool $showheader = FALSE;
/** @var array Our compiled page */
protected array $build;
/* Fields */
// Current field being edited
private ?int $field_active = NULL;
/** @var Collection Dynamic fields that are pre-populated with system data */
protected Collection $fields_dynamic;
/** @var Collection Input fields take input from the user */
protected Collection $fields_input;
protected bool $debug;
abstract public function attr(array $field): string;
abstract public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array;
abstract public static function strlenv($text): int;
public function __construct(int $frame,string $index='a',bool $debug=FALSE)
{
$this->debug = $debug;
$this->layout = new Window(1,1,static::FRAME_WIDTH,static::FRAME_HEIGHT+1,'LAYOUT',NULL,$debug);
$this->header = new Window(1,1,static::FRAME_WIDTH,1,'HEADER',$this->layout,$debug);
//dump(['this'=>get_class($this),'header_from'=>$this->header->x,'header_to'=>$this->header->bx,'width'=>$this->header->width]);
// Provider can use all its space
$this->provider = new Window(1,1,static::FRAME_PROVIDER_LENGTH,1,'PROVIDER',$this->header,$debug);
//dump(['this'=>get_class($this),'provider_from'=>$this->provider->x,'provider_to'=>$this->provider->bx,'width'=>$this->provider->width]);
// Page number is prefixed with a color change (if required, otherwise a space)
$this->pagenum = new Window($this->provider->bx+1,1,static::FRAME_PAGE_LENGTH,1,'#',$this->header,$debug);
//dump(['this'=>get_class($this),'pagenum_from'=>$this->pagenum->x,'pagenum_to'=>$this->pagenum->bx,'width'=>$this->pagenum->width]);
// Unit is prefixed with a color change (required, since a different color to page)
$this->unit = new Window($this->pagenum->bx+1,1,static::FRAME_COST_LENGTH,1,'$',$this->header,$debug);
//dump(['this'=>get_class($this),'unit_from'=>$this->unit->x,'unit_to'=>$this->unit->bx,'width'=>$this->unit->width]);
$this->content = new Window(1,2,static::FRAME_WIDTH,static::FRAME_HEIGHT,'CONTENT',$this->layout,$debug);
$this->resetHistory();
$this->clear();
$this->goto($frame,$index);
}
public function __get(string $key): mixed
{
switch ($key) {
case 'access' :
case 'id' :
case 'cls':
case 'cost':
case 'created_at':
case 'public' :
case 'type' :
return $this->fo?->{$key};
case 'cug': return $this->fo?->cug_id;
case 'frame':
case 'index':
return $this->{$key};
case 'next': return ($this->index < 'z') ? chr(ord($this->index)+1) : $this->index;
case 'prev': return ($this->index > 'a') ? chr(ord($this->index)-1) : $this->index;
case 'page': return sprintf('%d%s',$this->frame,$this->index);
case 'height': return $this->layout->height;
case 'width': return $this->layout->width;
case 'fields_input': return $this->fields_input;
case 'field_current': return (! is_null($this->field_active)) ? $this->fields_input->get($this->field_active): NULL;
default:
throw new \Exception('Unknown key: '.$key);
}
}
public function __set(string $key,mixed $value): void
{
switch ($key) {
case 'showheader':
$this->{$key} = $value;
break;
default:
throw new \Exception('Unknown key: '.$key);
}
}
public function __toString(): string
{
return $this->display()->join("");
}
/* METHODS */
/**
* Return a list of alternative versions of this frame.
*
* @todo: Need to adjust to not include access=0 frames unless owner
*/
public function alts(): Collection
{
return Frame::where('frame',$this->frame)
->where('index',$this->index)
->where('id','<>',$this->fo->id)
->where('mode_id',$this->id)
->where('access',1)
->limit(9)
->get();
}
private function atcode(string $name,int $length,mixed $pad=' '): string
{
switch ($name) {
case 'NODE':
$result = '00010001';
break;
case 'DATETIME':
$result = Carbon::now()->toRfc822String();
break;
case 'DATE':
$result = Carbon::now()->format('Y-m-d');
break;
case 'TIME':
$result = Carbon::now()->format('H:ia');
break;
default:
$result = $name;
}
if (strlen($result) < abs($length) && $pad)
$result = ($length < 0)
? Str::padLeft($result,abs($length),$pad)
: Str::padRight($result,abs($length),$pad);
return $result;
}
/**
* History go back to previous page
*
* @return bool
*/
public function back(): bool
{
if ($this->history->count() > 1) {
$this->history->pop();
$this->fo = $this->history->last();
return TRUE;
}
return FALSE;
}
/**
* Parse a page, extracting fields and formatting into our Window objects
*
* @param bool $force
* @return array
* @throws \Exception
*/
public function build(bool $force=FALSE): array
{
if ($this->build && ! $force)
throw new \Exception('Refusing to build without force.');
$this->load();
$test = FALSE;
$this->provider->content = $this->parse(($test ? chr(0x02).'T'.chr(0x03).'B'.chr(0x04) : 'TB').'A'.($test ? ' - 12345678901234567890123456789012345678901234567890123456' : ''),static::FRAME_PROVIDER_LENGTH,$this->provider->y,$this->provider->x);
$this->pagenum->content = $this->parse($this->color_page.($test ? '123456789012345a' : $this->page),static::FRAME_SPACE+static::FRAME_PAGE_LENGTH,$this->pagenum->y,$this->pagenum->x);
$this->unit->content = $this->parse($this->color_unit.Str::padLeft(($this->cost+($test ? 1234 : 0)).'c',static::FRAME_COST_LENGTH-1,' '),static::FRAME_SPACE+static::FRAME_COST_LENGTH,$this->unit->y,$this->unit->x);
$this->content->content = $this->parse($this->fo->content,static::FRAME_WIDTH,$this->content->y,$this->content->x);
$this->header->visible = ($this->showheader || $test);
$this->build_system_fields();
$this->build = $this->layout->build(1,1,$this->debug);
// Add our dynamic values
$fields = $this->fields_dynamic->filter(function($item) { return $item->value; });
Log::channel('bbs')->debug(sprintf('There are [%d] dynamic fields to populate',$fields->count()));
if ($fields->count())
$this->fields_insert($fields);
// Add our input fields
$fields = $this->fields_input->filter(function($item) { return is_null($item->value); });
Log::channel('bbs')->debug(sprintf('There are [%d] input fields to setup',$fields->count()));
if ($fields->count())
$this->fields_insert($fields);
return $this->build;
}
// @todo To complete - some of these came from SBBS and are not valid here
private function build_system_fields(): void
{
// Fields we can process automatically
$auto = ['NODE','DATETIME','DATE','TIME','REALNAME','BBS'];
$df = $this->fields_dynamic->filter(function($item) { return is_null($item->value); });
if (! $df->count())
return;
foreach ($df as $field) {
if (in_array($field->name,$auto))
$this->field_dynamic($field->name,$this->atcode($field->name,$field->size,$field->pad));
}
}
private function clear(): void
{
$this->build = [];
$this->fields_dynamic = collect();
$this->fields_input = collect();
$this->fieldReset();
}
// Insert our *_field data (if it is set)
public function display(): Collection
{
if (! $this->build)
throw new \Exception('Page not ready');
// build
$display = $this->build;
// populate dynamic fields - refresh dynamic fields if 09, otherwise show previous compiled with 00
// check if there are any dynamic fields with no values
switch ($this->mo->name) {
case 'ansi':
$new_line = NULL;
$shownullchars = TRUE;
break;
case 'viewdata':
$new_line = static::BG_BLACK|static::WHITE;
$shownullchars = FALSE;
break;
default:
throw new \Exception(sprintf('Dont know how to display a [%s] page',$this->mo->name));
}
$result = collect();
$last = $new_line;
if ($this->debug)
dump(['page-width'=>$this->width,'page-height'=>$this->height]);
// render
for ($y=1;$y<=$this->height;$y++) {
$line = '';
if ($new_line)
$last = $new_line;
if ($this->debug)
dump('============== ['.$y.'] ===============');
$x = 1;
while ($x <= $this->width) {
if ($this->debug)
dump('* CELL : y:'.$y.', x:'.$x);
// The current char value
$char = (isset($display[$y]) && isset($display[$y][$x])) ? $display[$y][$x] : NULL;
if ($this->debug)
dump(' - CHAR : '.(! is_null($char) ? $char->ch : 'undefined').', ATTR:'.(! is_null($char) ? $char->attr : 'undefined').', LAST:'.$last);
if ($this->debug) {
dump('-------- ['.$x.'] ------');
dump('y:'.$y.',x:'.$x.', attr:'.(! is_null($char) ? $char->attr : 'undefined'));
}
// Only write a new attribute if it has changed (and not Videotex)
if ($last !== $char->attr) {
// The current attribute for this character
$attr = is_null($char) ? NULL : $char->attr($this->mo,$last,$this->debug);
switch ($this->mo->name) {
case 'ansi':
// If the attribute is null, we'll write our default attribute
if (is_null($attr))
$line .= ''; #static::BG_BLACK|static::LIGHTGRAY;
else
$line .= (! is_null($attr)) ? $attr : '';
break;
case 'viewdata':
// If the attribute is null, we'll ignore it since we are drawing a character
if (! is_null($attr)) {
if ($this->debug)
dump(sprintf('= SEND attr:%02x, last: %02x [%s] (%s)',ord($attr),$last,$char->ch,serialize($attr)));
$line .= "\x1b".$attr;
//$x++;
}
break;
default:
throw new \Exception(sprintf('[%s] has not been implemented',$this->mo->name));
}
}
if (! is_null($char->ch)) {
if ($this->debug)
dump(' = SEND CHAR :'.$char->ch.', attr:'.$char->attr.', last:'.$last);
$line .= $char->ch;
} else if ($shownullchars || ((is_null($char->ch) && is_null($char->attr)))) {
if ($this->debug)
dump(' = CHAR UNDEFINED');
$line .= ' ';
}
$last = $char->attr;
$x++;
}
if ($this->debug)
dump(['line'=>$line]);
$result->push($line);
if ($this->debug && ($y > $this->debug))
exit(1);
}
return $result;
}
/**
* Update a dynamic field with a value
*
* @param $name
* @param $value
* @return void
* @throws \Exception
*/
private function field_dynamic($name,$value): void
{
if (($x=$this->fields_dynamic->search(function($item) use ($name) { return $item->name === $name; })) !== FALSE) {
$field = $this->fields_dynamic->get($x);
// Store our value
$field->value = $value;
} else {
throw new \Exception(sprintf('Dynamic field: [%s], doesnt exist?',$name));
}
}
private function fields_insert($fields) {
foreach ($fields as $field) {
if (is_null($field->value))
continue;
$content = str_split($field->value);
$y = $field->y;
$x = $field->x;
for ($x;$x < $field->x+abs($field->size);$x++) {
$index = $x-$field->x;
if (isset($content[$index]))
$this->build[$y][$x]->ch = ($field->type !== 'p') ? $content[$index] : '*';
else
$this->build[$y][$x]->ch = $field->pad;
}
}
}
public function fieldReset(): void
{
$this->field_active = NULL;
foreach ($this->fields_input as $field)
$field->value = NULL;
}
public function fieldNext(): Field|NULL
{
if ($this->fields_input->count()) {
if (is_null($this->field_active))
$this->field_active = 0;
else
$this->field_active++;
return $this->fields_input->get($this->field_active);
} else
return NULL;
}
/**
* Load a frame by it's ID.
*
* @param int $id
* @return void
*/
public function get(int $id): void
{
$this->po->findOrFail($id);
$this->frame = $this->po->frame;
$this->index = $this->po->index;
}
/**
* Go to a specific frame
*
* @param int $frame
* @param string $index
* @return void
* @throws \Exception
*/
public function goto(int $frame,string $index='a'): void
{
if (strlen($index) !== 1)
throw new \Exception('Invalid index:'.$index);
$this->frame = $frame;
$this->index = $index;
$this->fo = NULL;
}
public function haveNext(): bool
{
return $this->fo
? Frame::where('frame',$this->frame)
->where('index',$this->next)
->where('mode_id',$this->fo->mode_id)
->exists()
: FALSE;
}
public function isCug(int $cug): bool
{
return $this->cug === $cug;
}
// @todo To implement
public function isOwner(User $o): bool
{
return FALSE;
}
public function isRoute(int $route): bool
{
return is_numeric($this->fo->{sprintf('r%d',$route)});
}
/**
* Load a frame, throw a model not found exception if it doesnt exist
*
* @return void
*/
public function load(): void
{
$this->fo = Frame::where('mode_id',$this->mo->id)
->where('frame',$this->frame)
->where('index',$this->index)
->orderBy('created_at','DESC')
->firstOrFail();
$this->history->push($this->fo);
$this->clear();
}
public function method(int $route): ?Action
{
if (($x=($this->fo->{sprintf('r%d',$route)})) && (! $this->isRoute($route)))
return Action::factory($x);
return NULL;
}
public function new(int $frame,string $index='a'): void
{
$this->frame = $frame;
$this->index = $index;
$this->fo = new Frame;
// Make sure parent frame exists
if (($this->index !== 'a') && (! Frame::where('frame',$this->frame)->where('index',$this->prev)->where('mode',$this->mo->id)->exists()))
throw new ParentNotFoundException(sprintf('Parent %d%s doesnt exist',$frame,$index));
}
public function next(): void
{
$this->index = $this->next;
$this->fo = NULL;
}
/**
* Clear a user's history
*
* @return void
*/
public function resetHistory(): void
{
$this->history = collect();
}
public function route(int $route): void
{
if ($this->isRoute($route)) {
$this->frame = (int)$this->fo->{sprintf('r%d',$route)};
$this->index = 'a';
$this->fo = NULL;
} else {
throw new NoRouteException('Invalid route '.$route);
}
}
public function prev(): void
{
$this->index = $this->prev;
$this->fo = NULL;
}
}

View File

@ -0,0 +1,433 @@
<?php
namespace App\Classes\BBS\Page;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use App\Classes\BBS\Frame\{Char,Field};
use App\Classes\BBS\Page;
use App\Models\BBS\Mode;
class Ansi extends Page
{
protected const FRAME_WIDTH = 80;
protected const FRAME_HEIGHT = 22;
protected const FRAME_PROVIDER_LENGTH = 55;
protected const FRAME_PAGE_LENGTH = 17; // Full space for page number + space at beginning (as would be displayed by viewdata)
protected const FRAME_COST_LENGTH = 8; // Full space for cost + space at beginning (as would be displayed by viewdata)
protected const FRAME_SPACE = 1; // Since colors dont take a space, this is to buffer a space
public const ESC = 27;
public const I_CLEAR_CODE = 0;
public const I_HIGH_CODE = 1;
public const I_BLINK_CODE = 5;
public const FG_WHITE_CODE = self::FG_LIGHTGRAY_CODE;
public const FG_YELLOW_CODE = self::FG_BROWN_CODE;
public const FG_BLACK_CODE = 30;
public const FG_RED_CODE = 31;
public const FG_GREEN_CODE = 32;
public const FG_BROWN_CODE = 33;
public const FG_BLUE_CODE = 34;
public const FG_MAGENTA_CODE = 35;
public const FG_CYAN_CODE = 36;
public const FG_LIGHTGRAY_CODE = 37;
public const BG_BLACK_CODE = 40;
public const BG_RED_CODE = 41;
public const BG_GREEN_CODE = 42;
public const BG_BROWN_CODE = 43;
public const BG_YELLOW_CODE = self::BG_BROWN_CODE;
public const BG_BLUE_CODE = 44;
public const BG_MAGENTA_CODE = 45;
public const BG_CYAN_CODE = 46;
public const BG_LIGHTGRAY_CODE = 47;
public static function strlenv($text): int
{
return strlen($text ? preg_replace('/'.ESC.'\[[0-9;?]+[a-zA-Z]/','',$text) : $text);
}
public function __construct(int $frame,string $index='a')
{
parent::__construct($frame,$index);
$this->mo = Mode::where('name','Ansi')->single();
}
public function __get(string $key): mixed
{
switch ($key) {
case 'color_page':
return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_WHITE_CODE);
case 'color_unit':
return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_GREEN_CODE);
default:
return parent::__get($key);
}
}
public function attr(array $field): string
{
return sprintf('%s[%d;%d;%dm',ESC,$field['i'],$field['f'],$field['b']);
}
/**
* This function converts ANSI text into an array of attributes
*
* We include the attribute for every character, so that if a window is placed on top of this window, the edges
* render correctly.
*
* @param string $contents Our ANSI content to convert
* @param int $width Canvas width before we wrap to the next line
* @param int $yoffset fields offset when rendered (based on main window)
* @param int $xoffset fields offset when rendered (based on main window)
* @param int|null $debug Enable debug mode
* @return array
* @throws \Exception
*/
public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array
{
$result = [];
$lines = collect(explode("\r\n",$contents));
if ($debug)
dump(['lines'=>$lines]);
$i = 0; // Intensity
$bg = self::BG_BLACK; // Background color
$fg = self::LIGHTGRAY; // Foreground color
$attr = $fg + $bg + $i; // Attribute int
$default = ['i'=>0,'f'=>self::FG_LIGHTGRAY_CODE,'b'=>self::BG_BLACK_CODE];
$y = 0; // Line
$saved_x = NULL; // Cursor saved
$saved_y = NULL; // Cursor saved
$ansi = $default; // Our current attribute used for input fields
while ($lines->count() > 0) {
$x = 0;
$line = $lines->shift();
$result[$y+1] = [];
if ($this->debug) dump(['next line'=>$line,'length'=>strlen($line)]);
if (is_numeric($debug) && ($y > $debug)) {
dump(['exiting'=>serialize($debug)]);
exit(1);
}
while (strlen($line) > 0) {
if ($debug)
dump(['y:'=>$y,'attr'=>$attr,'line'=>$line,'length'=>strlen($line)]);
if ($x >= $width) {
$x = 0;
$y++;
}
/* parse an attribute sequence*/
$m = [];
preg_match('/^\x1b\[((\d+)+(;(\d+)+)*)m/U',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
// Are values separated by ;
$m = array_map(function($item) { return (int)$item; },explode(';',$m[0]));
// Sort our numbers
sort($m);
// Reset
if ($m[0] === self::I_CLEAR_CODE) {
$bg = self::BG_BLACK;
$fg = self::LIGHTGRAY;
$i = 0;
$ansi = $default;
array_shift($m);
}
// High Intensity
if (count($m) && ($m[0] === self::I_HIGH_CODE)) {
$i += ((($i === 0) || ($i === self::BLINK)) ? self::HIGH : 0);
$ansi['i'] = self::I_HIGH_CODE;
array_shift($m);
}
// Blink
if (count($m) && ($m[0] === self::I_BLINK_CODE)) {
$i += ((($i === 0) || ($i === self::HIGH)) ? self::BLINK : 0);
array_shift($m);
}
// Foreground
if (count($m) && ($m[0] >= self::FG_BLACK_CODE) && ($m[0] <= self::FG_LIGHTGRAY_CODE)) {
$ansi['f'] = $m[0];
switch (array_shift($m)) {
case self::FG_BLACK_CODE:
$fg = self::BLACK;
break;
case self::FG_RED_CODE:
$fg = self::RED;
break;
case self::FG_GREEN_CODE:
$fg = self::GREEN;
break;
case self::FG_YELLOW_CODE:
$fg = self::BROWN;
break;
case self::FG_BLUE_CODE:
$fg = self::BLUE;
break;
case self::FG_MAGENTA_CODE:
$fg = self::MAGENTA;
break;
case self::FG_CYAN_CODE:
$fg = self::CYAN;
break;
case self::FG_LIGHTGRAY_CODE:
$fg = self::LIGHTGRAY;
break;
}
}
// Background
if (count($m) && ($m[0] >= self::BG_BLACK_CODE) && ($m[0] <= self::BG_LIGHTGRAY_CODE)) {
$ansi['b'] = $m[0];
switch (array_shift($m)) {
case self::BG_BLACK_CODE:
$bg = self::BG_BLACK;
break;
case self::BG_RED_CODE:
$bg = self::BG_RED;
break;
case self::BG_GREEN_CODE:
$bg = self::BG_GREEN;
break;
case self::BG_BROWN_CODE:
$bg = self::BG_BROWN;
break;
case self::BG_BLUE_CODE:
$bg = self::BG_BLUE;
break;
case self::BG_MAGENTA_CODE:
$bg = self::BG_MAGENTA;
break;
case self::BG_CYAN_CODE:
$bg = self::BG_CYAN;
break;
case self::BG_LIGHTGRAY_CODE:
$bg = self::BG_LIGHTGRAY;
break;
}
}
$attr = $bg + $fg + $i;
continue;
}
/* parse absolute character position */
$m = [];
preg_match('/^\x1b\[(\d*);?(\d*)[Hf]/',$line,$m);
if (count($m)) {
dump(['Hf'=>$m]); // @todo Remove once validated
$line = substr($line,strlen(array_shift($m)));
$y = (int)array_shift($m);
if (count($m))
$x = (int)array_shift($m)-1;
continue;
}
/* ignore an invalid sequence */
$m = [];
preg_match('/^\x1b\[\?7h/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
continue;
}
/* parse positional sequences */
$m = [];
preg_match('/^\x1b\[(\d+)([A-D])/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
switch ($m[1]) {
/* parse an up positional sequence */
case 'A':
$y -= ($m[0] < 1) ? 0 : $m[0];
break;
/* parse a down positional sequence */
case 'B':
$y += ($m[0] < 1) ? 0 : $m[0];
break;
/* parse a forward positional sequence */
case 'C':
$x += ($m[0] < 1) ? 0 : $m[0];
break;
/* parse a backward positional sequence */
case 'D':
$x -= ($m[0] < 1) ? 0 : $m[0];
break;
}
continue;
}
/* parse a clear screen sequence - we ignore them */
$m = [];
preg_match('/^\x1b\[2J/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
continue;
}
/* parse cursor sequences */
$m = [];
preg_match('/^\x1b\[([su])/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
switch ($m[0]) {
/* parse save cursor sequence */
case 's':
$saved_x = $x;
$saved_y = $y;
break;
/* parse restore cursor sequence */
case 'u':
$x = $saved_x;
$y = $saved_y;
break;
}
continue;
}
/* parse an input field */
// Input field 'FIELD;valueTYPE;input char'
// @todo remove the trailing ESC \ to end the field, just use a control code ^B \x02 (Start of Text) and ^C \x03
$m = [];
preg_match('/^\x1b_([A-Z]+;[0-9a-z]+)([;]?.+)?\x1b\\\/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
// We are interested in our field match
$f = explode(';',array_shift($m));
// First value is the field name
$field = array_shift($f);
// Second value is the length/type of the field, nnX nn=size in chars, X=type (lower case)
$c = [];
preg_match('/([0-9]+)([a-z])/',$xx=array_shift($f),$c);
if (! count($c)) {
Log::channel('bbs')->alert(sprintf('! IF FAILED PARSING FIELD LENGTH/TYPE [%02dx%02d] (%s)',$y,$x,$xx));
break;
}
// Third field is the char to use
$fieldpad = count($f) ? array_shift($f) : '.';
Log::channel('bbs')->info(sprintf('- IF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$c[2],$c[1],$fieldpad));
// Any remaining fields are junk
if (count($f))
Log::channel('bbs')->alert(sprintf('! IGNORING ADDITIONAL IF FIELDS [%02dx%02d] (%s)',$y,$x,join('',$f)));
// If we are padding our field with a char, we need to add that back to $line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
if ($c[1])
$line = str_repeat($fieldpad,$c[1]).$line;
$this->fields_input->push(new Field([
'attribute' => $ansi,
'name' => $field,
'pad' => $fieldpad,
'size' => $c[1],
'type' => $c[2],
'value' => NULL,
'x' => $x+$xoffset,
'y' => $y+$yoffset,
]));
}
/* parse dynamic value field */
// @todo remove the trailing ESC \ to end the field, just use a control code ie: ^E \x05 (Enquiry) or ^Z \x26 (Substitute)
$m = [];
preg_match('/^\x1bX([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?\x1b\\\/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
// We are interested in our field match
$f = explode(';',array_shift($m));
$pad = Arr::get($f,2,' ');
Log::channel('bbs')->info(sprintf('- DF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$f[0],$f[1],$pad));
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
$line = str_repeat($pad,abs($f[1])).$line;
$this->fields_dynamic->push(new Field([
'name' => $f[0],
'pad' => $pad,
'type' => NULL,
'size' => $f[1],
'value' => NULL,
'x' => $x+$xoffset,
'y' => $y+$yoffset,
]));
}
/* set character and attribute */
$ch = $line[0];
$line = substr($line,1);
/* validate position */
if ($y < 0)
$y = 0;
if ($x < 0)
$x = 0;
if ($attr === null)
throw new \Exception('Attribute is null?');
$result[$y+1][$x+1] = new Char($ch,$attr);
$x++;
}
// If we got a self::BG_BLACK|self::LIGHTGRAY ESC [0m, but not character, we include it as it resets any background that was going on
if (($attr === self::BG_BLACK|self::LIGHTGRAY) && isset($result[$y+1][$x]) && ($result[$y+1][$x]->attr !== $attr))
$result[$y+1][$x+1] = new Char(NULL,$attr);
$y++;
}
return $result;
}
}

View File

@ -0,0 +1,370 @@
<?php
namespace App\Classes\BBS\Page;
use Illuminate\Support\Arr;
use App\Classes\BBS\Frame\{Char,Field};
use App\Classes\BBS\Page;
use App\Models\BBS\Mode;
class Viewdata extends Page
{
protected const FRAME_WIDTH = 40;
protected const FRAME_HEIGHT = 22;
protected const FRAME_PROVIDER_LENGTH = 23;
protected const FRAME_PAGE_LENGTH = 11; // Spec is 9+1 - including our color code.
protected const FRAME_COST_LENGTH = 6; // including our color code
protected const FRAME_SPACE = 0; // Since colors take a space, this is not needed
public const MOSIAC = 0x10;
// Toggles
public const CONCEAL = 0x20;
public const REVEAL = 0x2000; // @temp Turns off Conceal
public const SEPARATED = 0x40;
public const BLOCKS = 0x4000; // @temp Turns off Separated
public const STEADY = 0x8000; // @temp (turn off flash)
public const DOUBLE = 0x100;
public const NORMAL = 0x1000; // @temp Turns off Double Height
public const HOLD = 0x200;
public const RELEASE = 0x20000; // @temp turns off Hold
public const NEWBACK = 0x400;
public const BLACKBACK = 0x800;
//public const ESC = 27;
//public const I_CLEAR_CODE = 0;
//public const I_HIGH_CODE = 1;
public const FG_BLACK_CODE = 0x40;
public const FG_RED_CODE = 0x41;
public const FG_GREEN_CODE = 0x42;
public const FG_YELLOW_CODE = 0x43;
public const FG_BLUE_CODE = 0x44;
public const FG_MAGENTA_CODE = 0x45;
public const FG_CYAN_CODE = 0x46;
public const FG_WHITE_CODE = 0x47;
public const I_BLINK_CODE = 0x48;
public const I_STEADY = 0x49;
public const I_NORMAL = 0x4c;
public const I_DOUBLE_CODE = 0x4d;
public const I_CONCEAL = 0x58;
public const I_BLOCKS = 0x59;
public const I_SEPARATED = 0x5a;
public const I_BLACKBACK = 0x5c;
public const I_NEWBACK = 0x5d;
public const I_HOLD = 0x5e;
public const I_REVEAL = 0x5f;
public const RED = 1;
//public const GREEN = 2;
public const YELLOW = 3;
public const BLUE = 4;
//public const MAGENTA = 5;
public const CYAN = 6;
public const WHITE = 7;
public const MOSIAC_RED_CODE = 0x51;
public const MOSIAC_GREEN_CODE = 0x52;
public const MOSIAC_YELLOW_CODE = 0x53;
public const MOSIAC_BLUE_CODE = 0x54;
public const MOSIAC_MAGENTA_CODE = 0x55;
public const MOSIAC_CYAN_CODE = 0x56;
public const MOSIAC_WHITE_CODE = 0x57; // W
public const input_map = [
'd' => 'DATE',
'e' => 'EMAIL',
'f' => 'FULLNAME',
'n' => 'USER',
'p' => 'PASS',
't' => 'TIME',
'y' => 'NODE',
'z' => 'TOKEN',
];
public static function strlenv($text):int
{
return strlen($text)-substr_count($text,ESC);
}
public function __construct(int $frame,string $index='a')
{
parent::__construct($frame,$index);
$this->mo = Mode::where('name','Viewdata')->single();
}
public function __get(string $key): mixed
{
switch ($key) {
case 'color_page':
return chr(self::WHITE);
case 'color_unit':
return chr(self::GREEN);
default:
return parent::__get($key);
}
}
public function attr(array $field): string
{
// Noop
return '';
}
/**
* This function converts Viewtex BIN data into an array of attributes
*
* With viewdata, a character is used/display regardless of whether it is a control character, or an actual display
* character.
*
* @param string $contents Our ANSI content to convert
* @param int $width Canvas width before we wrap to the next line
* @param int $yoffset fields offset when rendered (based on main window)
* @param int $xoffset fields offset when rendered (based on main window)
* @param int|null $debug Enable debug mode
* @return array
* @throws \Exception
*/
public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array
{
$result = [];
$lines = collect(explode("\r\n",$contents));
if ($debug)
dump(['lines'=>$lines]);
$i = 0; // Intensity
$bg = self::BG_BLACK; // Background color
$fg = self::WHITE; // Foreground color
$new_line = $fg + $bg + $i; // Attribute int
// Attribute state on a new line
$attr = $new_line;
$y = 0;
while ($lines->count() > 0) {
$x = 0;
$line = $lines->shift();
$result[$y+1] = [];
if ($this->debug)
dump(['next line'=>$line,'length'=>strlen($line)]);
while (strlen($line) > 0) {
if ($debug)
dump(['y:'=>$y,'attr'=>$attr,'line'=>$line,'length'=>strlen($line)]);
if ($x >= $width) {
$x = 0;
// Each new line, we reset the attrs
$attr = $new_line;
$y++;
}
/* parse control codes */
$m = [];
preg_match('/^([\x00-\x09\x0c-\x1a\x1c-\x1f])/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
$attr = 0;
switch ($xx=ord(array_shift($m))) {
case 0x00:
$attr += self::BLACK;
break;
case 0x01:
$attr += self::RED;
break;
case 0x02:
$attr += self::GREEN;
break;
case 0x03:
$attr += self::YELLOW;
break;
case 0x04:
$attr += self::BLUE;
break;
case 0x05:
$attr += self::MAGENTA;
break;
case 0x06:
$attr += self::CYAN;
break;
case 0x07:
$attr += self::WHITE;
break;
case 0x08:
$attr = self::BLINK;
break;
case 0x09:
$attr = self::STEADY;
break;
/*
case 0x0a:
//$attr = self::ENDBOX; // End Box (Unused?)
break;
case 0x0b:
//$attr = self::STARTBOX; // Start Box (Unused?)
break;
*/
case 0x0c:
$attr = self::NORMAL;
break;
case 0x0d:
$attr = self::DOUBLE;
break;
case 0x0e:
$attr = self::NORMAL; // @todo Double Width (Unused)?
break;
case 0x0f:
$attr = self::NORMAL; // @todo Double Width (Unused?)
break;
case 0x10:
$attr = self::MOSIAC|self::BLACK;
break;
case 0x11:
$attr = self::MOSIAC|self::RED;
break;
case 0x12:
$attr = self::MOSIAC|self::GREEN;
break;
case 0x13:
$attr = self::MOSIAC|self::YELLOW;
break;
case 0x14:
$attr = self::MOSIAC|self::BLUE;
break;
case 0x15:
$attr = self::MOSIAC|self::MAGENTA;
break;
case 0x16:
$attr = self::MOSIAC|self::CYAN;
break;
case 0x17:
$attr = self::MOSIAC|self::WHITE;
break;
case 0x18:
$attr = self::CONCEAL;
break;
case 0x19:
$attr = self::BLOCKS;
break;
case 0x1a:
$attr = self::SEPARATED;
break;
/*
// We are using this for field input
case 0x1b:
//$attr = self::NORMAL; // CSI
break;
*/
case 0x1c:
$attr = self::BLACKBACK; // Black Background
break;
case 0x1d:
$attr = self::NEWBACK; // New Background
break;
case 0x1e:
$attr = self::HOLD; // Mosiac Hold
break;
case 0x1f:
$attr = self::RELEASE; // Mosiac Release
break;
// Catch all for other codes
default:
dump(['char'=>$xx]);
$attr = 0xff00;
}
if ($debug)
dump(sprintf('- got control code [%02x] at [%02dx%02d]',$attr,$y,$x));
$result[$y+1][$x+1] = new Char(NULL,$attr);
$x++;
continue;
}
/**
* For response frames, a dialogue field is signalled by a CLS (0x0c) followed by a number of dialogue
* characters [a-z]. The field ends by the first different character from the initial dialogue character.
* The CLS is a "privileged space" and the dialogue characters defined the dialogue field.
*
* Standard dialogue characters:
* + n = name
* + t = telephone number
* + d = date and time
* + a = address
* + anything else free form, typically 'f' is used
*
* Source: Prestel Bulk Update Technical Specification
*/
/* parse an input field */
// Since 0x0c is double, we'll use good ol' ESC 0x1b
$m = [];
preg_match('/^([\x1b|\x9b])([a-z])\2+/',$line,$m);
if (count($m)) {
$line = substr($line,strlen($m[0]));
$len = strlen(substr($m[0],1));
$field = new Field([
'attribute' => [],
'name' => Arr::get(self::input_map,$m[2],$m[2]),
'pad' => '.',
'size' => $len,
'type' => $m[2],
'value' => NULL,
'x' => $x+$xoffset,
'y' => $y+$yoffset,
]);
(($m[1] === "\x1b") ? $this->fields_input : $this->fields_dynamic)->push($field);
$result[$y+1][++$x] = new Char(' ',$attr); // The \x1b|\x9b is the privileged space.
for ($xx=0;$xx<$len;$xx++)
$result[$y+1][$x+1+$xx] = new Char('.',$attr);
$x += $len;
continue;
}
/* set character and attribute */
$ch = $line[0];
$line = substr($line,1);
if ($debug)
dump(sprintf('Storing [%02xx%02x] [%s] with [%02x]',$y,$x,$ch,$attr));
/* validate position */
if ($y < 0)
$y = 0;
if ($x < 0)
$x = 0;
if ($attr === null)
throw new \Exception('Attribute is null?');
$result[$y+1][$x+1] = new Char($ch,$attr);
$x++;
}
// Each new line, we reset the attrs
$attr = $new_line;
$y++;
}
return $result;
}
}

1231
app/Classes/BBS/Server.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
<?php
namespace App\Classes\BBS\Server;
use App\Classes\BBS\Server as AbstractServer;
use App\Classes\Sock\SocketClient;
class Ansitex extends AbstractServer
{
protected const LOGKEY = 'BAS';
/* CONSTS */
public const PORT = 23;
protected function init(SocketClient $client)
{
define('ESC', chr(27));
define('CON', ESC.'[?25h'); // Cursor On
define('COFF', ESC.'[?25l'); // Cursor Off
define('CSAVE', ESC.'[s'); // Save Cursor position
define('CRESTORE',ESC.'[u'); // Restore to saved position
define('HOME', ESC.'[0;0f');
define('LEFT', ESC.'[D'); // Move Cursor
define('RIGHT', ESC.'[C'); // Move Cursor
define('DOWN', ESC.'[B'); // Move Cursor
define('UP', ESC.'[A'); // Move Cursor
define('CR', chr(13));
define('LF', chr(10));
define('BS', chr(8));
define('CLS', ESC.'[2J');
define('HASH', '#'); // Enter
define('STAR', '*'); // Star Entry
define('SPACE', ' '); // Space (for compatibility with Videotex)
// NOTE: This consts are effective output
define('RESET', ESC.'[0;39;49m');
define('RED', ESC.'[0;31m');
define('GREEN', ESC.'[0;32m');
define('YELLOW', ESC.'[1;33m');
define('BLUE', ESC.'[0;34m');
define('MAGENTA', ESC.'[0;35m');
define('CYAN', ESC.'[0;36m');
define('WHITE', ESC.'[1;37m');
define('NEWBG', '');
// Compatibility attributes (to Videotex).
define('R_RED', RED.SPACE);
define('R_GREEN', GREEN.SPACE);
define('R_YELLOW', YELLOW.SPACE);
define('R_BLUE', BLUE.SPACE);
define('R_MAGENTA', MAGENTA.SPACE);
define('R_CYAN', CYAN.SPACE);
define('R_WHITE', WHITE.SPACE);
//define('FLASH',chr(8));
// Keyboard presses
// @todo Check where these are used vs the keys defined above?
define('KEY_DELETE', chr(8));
define('KEY_LEFT', chr(136));
define('KEY_RIGHT', chr(137));
define('KEY_DOWN', chr(138));
define('KEY_UP', chr(139));
parent::init($client);
}
function moveCursor($x,$y): string
{
return ESC.'['.$y.';'.$x.'f';
}
// Abstract function
public function sendBaseline(string $text,bool $reposition=FALSE)
{
$this->client->send(CSAVE.ESC.'[24;0f'.RESET.SPACE.$text.
($this->blp > $this->po->strlenv(SPACE.$text)
? str_repeat(' ',$this->blp-$this->po->strlenv(SPACE.$text)).
($reposition ? ESC.'[24;0f'.str_repeat(RIGHT,$this->po->strlenv(SPACE.$text)) : CRESTORE)
: ($reposition ? '' : CRESTORE)),
static::TIMEOUT
);
$this->blp = $this->po->strlenv(SPACE.$text);
$this->baseline = $text;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Classes\BBS\Server;
use App\Classes\BBS\Server as AbstractServer;
use App\Classes\Sock\SocketClient;
class Videotex extends AbstractServer
{
protected const LOGKEY = 'BVS';
/* CONSTS */
public const PORT = 516;
protected function init(SocketClient $client)
{
define('ESC', chr(27));
define('CON', chr(17)); // Cursor On
define('COFF', chr(20)); // Cursor Off
define('HOME', chr(30));
define('LEFT', chr(8)); // Move Cursor
define('RIGHT', chr(9)); // Move Cursor
define('DOWN', chr(10)); // Move Cursor
define('UP', chr(11)); // Move Cursor
define('CR', chr(13));
define('LF', chr(10));
define('CLS', chr(12));
define('HASH', '_'); // Enter
define('STAR', '*'); // Star Entry
define('SPACE', ''); // Space
// NOTE: This consts are effective output
define('RESET', '');
define('RED', ESC.'A');
define('GREEN', ESC.'B');
define('YELLOW', ESC.'C');
define('BLUE', ESC.'D');
define('MAGENTA', ESC.'E');
define('CYAN', ESC.'F');
define('WHITE', ESC.'G');
define('NEWBG', ESC.']');
// Raw attributes - used when storing frames.
define('R_RED', chr(1));
define('R_GREEN', chr(2));
define('R_YELLOW', chr(3));
define('R_BLUE', chr(4));
define('R_MAGENTA', chr(5));
define('R_CYAN', chr(6));
define('R_WHITE', chr(7));
define('FLASH', chr(8));
define('KEY_DELETE', chr(0x7f));
define('KEY_LEFT', chr(0x08));
define('KEY_RIGHT', chr(0x09));
define('KEY_DOWN', chr(0x0a));
define('KEY_UP', chr(0x0b));
parent::init($client);
}
public function moveCursor($x,$y): string
{
// Take the shortest path.
if ($y < 12) {
return HOME.
(($x < 21)
? str_repeat(DOWN,$y-1).str_repeat(RIGHT,$x)
: str_repeat(DOWN,$y).str_repeat(LEFT,40-$x));
} else {
return HOME.str_repeat(UP,24-$y+1).
(($x < 21)
? str_repeat(RIGHT,$x)
: str_repeat(LEFT,40-$x));
}
}
public function sendBaseline(string $text,bool $reposition=FALSE) {
$this->client->send(HOME.UP.$text.
($this->blp > $this->po->strlenv($text)
? str_repeat(' ',$this->blp-$this->po->strlenv($text)).
($reposition ? HOME.UP.str_repeat(RIGHT,$this->po->strlenv($text)) : '')
: ''),
static::TIMEOUT
);
$this->blp = $this->po->strlenv($text);
}
}

365
app/Classes/BBS/Window.php Normal file
View File

@ -0,0 +1,365 @@
<?php
namespace App\Classes\BBS;
use Illuminate\Support\Collection;
use App\Classes\BBS\Frame\Char;
/**
* Windows are elements of a Page object
*
* @param int $x - (int) starting x of it's parent [1..]
* @param int $y - (int) starting y of it's parent [1..]
* @param int $width - (int) full width of the window (text content will be smaller if there are scroll bars/boarder)
* @param int $height - (int) full height of the window (text content will be smaller if there are scroll bars/boarder)
* @param string $name - (string) internal name for the window (useful for debugging)
* @param Window $parent - (object) parent of this window
* @param bool $debug - (int) debug mode, which fills the window with debug content
*
* Pages have the following attributes:
* - bx/by - (int) right/bottom most boundary of the window representing the start + width/height of the window
* - child - (array) children in this window
* - height - (int) Window's height
* - name - (string) Windows name (useful for internal debugging)
* - parent - (object) Parent that this window belongs to
* - x/y - (int) start position of the window
* - visible - (bool) whether this window is visible
* - width - (int) Window's width
* - z - (int) Window's depth indicator
*
* Windows have the following public functions
* - build - Compile the frame for rendering
* - debug - Useful for debugging with properties of this Window
* - draw - Draw a part of this Window
*/
class Window
{
/** @var int X offset of parent that the canvas starts [1..width] */
private int $x;
/** @var int Y offset of parent that the canvas starts [1..height] */
private int $y;
/** @var int Window top-bottom position, higher z is shown [0..] */
private int $z = 0;
/** @var int When canvas width > width, this is the offset we display [0..] */
private int $ox = 0;
/** @var int When canvas height > height, this is the offset we display [0..] */
private int $oy = 0;
/** @var int Display Width + (1 char if scrollbars = true) */
private int $width;
/** @var int Display Height */
private int $height;
/** @var int Width of Canvas (default display width) */
private int $canvaswidth;
/** @var int Height of Canvas (default display height) */
private int $canvasheight;
/** @var array Window content - starting at 0,0 = 1,1 */
public array $content = [];
/** @var bool Window visible */
private bool $visible = TRUE;
/** @var string Window name */
private string $name;
/** @var bool Can this frame move outside the parent */
private bool $checkbounds = TRUE;
/** @var bool Can the content scroll vertically (takes up 1 line) [AUTO DETERMINE IF canvas > width] */
private bool $v_scroll = TRUE;
/** @var bool Can the content scroll horizontally (takes up 1 char) [AUTO DETERMINE IF canvas > height] */
private bool $h_scroll = FALSE;
/** @var int|bool Overflowed content is rendered with the next page */
private bool $pageable = FALSE;
private Page|Window|NULL $parent;
private Collection $child;
private bool $debug;
/*
Validation to implement:
+ X BOUNDARY
- x cannot be < parent.x if checkbounds is true [when moving window]
- x+width(-1 if h_scroll is true) cannot be greater than parent.width if checkbounds is true
- v_scroll must be true for canvaswidth > width
- when scrolling ox cannot be > width-x
- when layout.pageable is true, next page will only have windows included that have a y in the range
ie: if height is 44 (window is 22), next page is 23-44 and will only include children where y=23-44
+ Y BOUNDARY
- y cannot be < parent.y if checkbounds is true [when moving window]
- y+height(-1 if v_scroll is true) cannot be greater than parent.height if checkbounds is true
- h_scroll must be true for canvasheight > height
- when scrolling oy cannot be > height-y
- when layout.pageable is true, children height cannot be greater than parent.height - y.
*/
public function __construct(int $x,int $y,int $width,int $height,string $name,Window|Page $parent=NULL,bool $debug=FALSE) {
$this->x = $x;
$this->y = $y;
$this->name = $name;
$this->parent = $parent;
$this->debug = $debug;
$this->child = collect();
if ($parent instanceof self) {
$this->z = $parent->child->count()+1;
$this->parent = $parent;
$this->parent->child->push($this);
// Check that our height/widths is not outside our parent
if (($this->x < 1) || ($width > $this->parent->width))
throw new \Exception(sprintf('Window: %s width [%d] is beyond our parent\'s width [%d].',$name,$width,$this->parent->width));
if (($x > $this->parent->bx) || ($x+$width-1 > $this->parent->bx))
throw new \Exception(sprintf('Window: %s start x [%d] and width [%d] is beyond our parent\'s end x [%d].',$name,$x,$width,$this->parent->bx));
if (($this->y < 1) || ($height > $this->parent->height))
throw new \Exception(sprintf('Window: %s height [%d] is beyond our parent\'s height [%d].',$name,$height,$this->parent->height));
if (($y > $this->parent->by) || ($y+$height-1 > $this->parent->by))
throw new \Exception(sprintf('Window: %s start y [%d] and height [%d] is beyond our parent\'s end y [%s].',$name,$y,$height,$this->parent->by));
} elseif ($parent instanceof Page) {
$this->parent = $parent;
}
$this->width = $this->canvaswidth = $width;
$this->height = $this->canvasheight = $height;
if ($debug) {
$this->canvaswidth = $width*2;
$this->canvasheight = $height*2;
}
// Fill with data
for($y=1;$y<=$this->canvasheight;$y++) {
for($x=1;$x<=$this->canvaswidth;$x++) {
if (! isset($this->content[$y]))
$this->content[$y] = [];
$this->content[$y][$x] = $debug
? new Char((($x > $this->width) || ($y > $this->height)) ? strtoupper($this->name[0]) : strtolower($this->name[0]))
: new Char();
}
}
}
public function __get($key): mixed
{
switch ($key) {
case 'bx': return $this->x+$this->width-1;
case 'by': return $this->y+$this->height-1;
case 'checkbounds': return $this->checkbounds;
case 'child':
return $this->child->sort(function($a,$b) {return ($a->z < $b->z) ? -1 : (($b->z < $a->z) ? 1 : 0); });
case 'name':
return $this->name;
case 'height':
case 'parent':
case 'visible':
case 'width':
case 'x':
case 'y':
case 'z':
return $this->{$key};
default:
throw new \Exception('Unknown key: '.$key);
}
}
public function __set($key,$value): void
{
switch ($key) {
case 'child':
if ($value instanceof self)
$this->child->push($value);
else
throw new \Exception('child not an instance of Window()');
break;
case 'content':
$this->content = $value;
break;
case 'parent':
if ($this->parent)
throw new \Exception('parent already DEFINED');
else
$this->parent = $value;
break;
case 'visible':
$this->visible = $value;
break;
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* Build this window, returning an array of Char that will be rendered by Page
*
* @param int $xoffset - (int) This windows x position for its parent
* @param int $yoffset - (int) This windows y position for its parent
* @param bool $debug - (int) debug mode, which fills the window with debug content
* @return array
*/
public function build(int $xoffset,int $yoffset,bool $debug=FALSE): array
{
$display = [];
if ($debug) {
dump('********* ['.$this->name.'] *********');
dump('name :'.$this->name);
dump('xoff :'.$xoffset);
dump('yoff :'.$yoffset);
dump('x :'.$this->x);
dump('bx :'.$this->bx);
dump('ox :'.$this->ox);
dump('y :'.$this->y);
dump('by :'.$this->by);
dump('oy :'.$this->oy);
dump('lines :'.count(array_keys($this->content)));
//dump('content:'.join('',$this->content[1]));
}
if ($debug)
dump('-------------');
for ($y=1;$y<=$this->height;$y++) {
if ($debug)
echo sprintf('%02d',$y).':';
$sy = $this->y-1+$y+$yoffset-1;
for ($x=1;$x<=$this->width;$x++) {
if ($debug)
dump('- Checking :'.$this->name.', y:'.($y+$this->oy).', x:'.($x+$this->ox));
$sx = $this->x-1+$x+$xoffset-1;
if (! isset($display[$sy]))
$display[$sy] = [];
if (isset($this->content[$y+$this->oy]) && isset($this->content[$y+$this->oy][$x+$this->ox])) {
$display[$sy][$sx] = $this->content[$y+$this->oy][$x+$this->ox];
if ($debug)
dump('- storing in y:'.($sy).', x:'.($sx).', ch:'.$display[$sy][$sx]->ch);
} else {
$display[$sy][$sx] = new Char();
if ($debug)
dump('- nothing for y:'.($sy).', x:'.($sx).', ch:'.$display[$sy][$sx]->ch);
}
}
if ($debug)
dump('---');
}
if ($debug)
dump('----LOOKING AT CHILDREN NOW---------');
if ($debug) {
dump('Window:'.$this->name.', has ['.$this->child->filter(function($child) { return $child->visible; })->count().'] children');
$this->child->each(function($child) {
dump(' - child:'.$child->name.', visible:'.$child->visible);
});
}
// Fill the array with our values
foreach ($this->child->filter(function($child) { return $child->visible; }) as $child) {
if ($debug) {
dump('=========== ['.$child->name.'] =============');
dump('xoff :'.$xoffset);
dump('yoff :'.$yoffset);
dump('x :'.$this->x);
dump('y :'.$this->y);
}
$draw = $child->build($this->x+$xoffset-1,$this->y+$yoffset-1,$debug);
if ($debug)
dump('draw y:'.join(',',array_keys($draw)));
foreach (array_keys($draw) as $y) {
foreach (array_keys($draw[$y]) as $x) {
if (! isset($display[$y]))
$display[$y] = [];
$display[$y][$x] = $draw[$y][$x];
}
}
if ($debug) {
//dump('draw 1:'.join(',',array_keys($draw[1])));
dump('=========== END ['.$child->name.'] =============');
}
}
if ($debug) {
dump('this->name:'.$this->name);
dump('this->y:'.$this->y);
dump('display now:'.join(',',array_values($display[$this->y])));
dump('********* END ['.$this->name.'] *********');
foreach ($display as $y => $data) {
dump(sprintf("%02d:%s (%d)\r\n",$y,join('',$data),count($data)));
}
}
return $display;
}
public function xdebug(string $text) {
return '- '.$text.': '.$this->name.'('.$this->x.'->'.($this->bx).') width:'.$this->width.' ['.$this->y.'=>'.$this->by.'] with z:'.$this->z;
}
/**
* Render this window
*
* @param $start - (int) Starting x position
* @param $end - (int) Ending x position
* @param $y - (int) Line to render
* @param $color - (bool) Whether to include color
* @returns {{x: number, content: string}}
*/
public function xdraw($start,$end,$y,$color): array
{
$content = '';
for ($x=$start;$x<=$end;$x++) {
$rx = $this->ox+$x;
$ry = $this->oy+$y;
// Check if we have an attribute to draw
if (! (isset($this->content[$ry])) || ! (isset($this->content[$ry][$rx]))) {
$content += ' ';
continue;
}
if ($color === NULL || $color === true) {
// Only write a new attribute if it has changed
if (($this->last === NULL) || ($this->last !== $this->content[$ry][$rx]->attr)) {
$this->last = $this->content[$ry][$rx]->attr;
$content += ($this->last === null ? BG_BLACK|LIGHTGRAY : $this->last);
}
}
try {
$content += ($this->content[$ry][$rx]->ch !== null ? $this->content[$ry][$rx]->ch : ' ');
} catch (\Exception $e) {
dump($e);
dump('---');
dump('x:'.($x-$this->x));
dump('y:'.($y-$this->y));
dump('ox:'.$this->ox);
dump('oy:'.$this->oy);
dump('$rx:'.$rx);
dump('$ry:'.$ry);
exit();
}
}
return ['content'=>$content, 'x'=>$end - $start + 1];
}
}

12
app/Classes/Dynamic.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes;
/**
* Dynamic files that are sent to systems during a mailer session
*/
abstract class Dynamic
{
abstract public function __toString(): string;
abstract public function getName(): string;
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Classes\Dynamic;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Classes\Dynamic;
use App\Models\Address;
/**
* This method will generate the hub status for an upstream Host/RC/ZC
*
* Arg is a collection of arguments. We only understand
* + name - name of file to give to remote
*/
class HubStats extends Dynamic
{
private const LOGKEY = 'DHS';
private string $name = '';
public function __construct(private Address $ao,Collection $arg)
{
Log::debug(sprintf('%s:- Generating Hub Stats for [%s] with arguments',self::LOGKEY,$ao->ftn),['args'=>$arg]);
$this->name = $arg->get('name');
}
public function __toString(): string
{
$date = Carbon::now()->yesterday()->endOfday();
$r = Address::select([
'a.id',
'addresses.system_id',
'addresses.zone_id',
'addresses.region_id',
'addresses.host_id',
'addresses.node_id',
'addresses.point_id',
'addresses.hub_id',
'addresses.role',
DB::raw('sum(a.uncollected_echomail) as uncollected_echomail'),
DB::raw('sum(a.uncollected_netmail) as uncollected_netmail'),
DB::raw('sum(a.uncollected_files) as uncollected_files')
])
->from(
Address::UncollectedEchomailTotal()
->where('echomails.created_at','<',$date)
->union(Address::UncollectedNetmailTotal()
->where('netmails.created_at','<',$date)
)
->union(Address::UncollectedFilesTotal()
->where('files.created_at','<',$date)
),'a')
->where('systems.active',true)
->where('addresses.active',TRUE)
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where('zones.id',$this->ao->zone_id)
->join('addresses',['addresses.id'=>'a.id'])
->join('systems',['systems.id'=>'addresses.system_id'])
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->ftnOrder()
->groupBy('addresses.system_id','a.id','addresses.zone_id','addresses.region_id','addresses.host_id','addresses.node_id','addresses.point_id','addresses.hub_id','addresses.role')
->with(['system','zone.domain']);
$header = "| %-12s | %4d | %3d | %3d | %16s | %5s | %5s |\r\n";
$output = sprintf("Hub Status for [%s] as at [%s]\r\n",our_address($this->ao)->ftn,$date);
$output .= "\r";
$output .= "+--------------+------+-----+-----+------------------+-------+-------+\r\n";
$output .= "| FTN | ECHO | NET |FILES| LAST SESSION | MODE |AUTOHLD|\r\n";
$output .= "+--------------+------+-----+-----+------------------+-------+-------+\r\n";
$havedown = FALSE;
$havehold = FALSE;
foreach($r->get() as $o) {
if ($o->uncollected_echomail > 10000)
$o->uncollected_echomail = 9999;
if ($o->uncollected_netmail > 10000)
$o->uncollected_netmail = 9999;
if ($o->uncollected_files > 10000)
$o->uncollected_files = 9999;
if ((! $havedown) && $o->is_down)
$havedown = TRUE;
if ((! $havehold) && $o->is_hold)
$havehold = TRUE;
$output .= sprintf($header,
sprintf('%s %s',$o->ftn4d,$o->is_down ? 'd' : ($o->is_hold ? 'h' : ' ')),
$o->uncollected_echomail ?? 0,
$o->uncollected_netmail ?? 0,
$o->uncollected_files ?? 0,
$o->system->last_session?->format('Y-m-d H:i'),
is_null($o->system->pollmode) ? 'HOLD' : ($o->system->pollmode ? 'CRASH' : 'DAILY'),
$o->system->autohold ? 'YES' : 'NO');
}
$output .= "+--------------+------+-----+-----+------------------+-------+-------+\r\n";
$output .= "\r\n";
if ($havehold)
$output .= "(h) Node is on HOLD status.\r\n";
if ($havedown)
$output .= "(d) Node is on DOWN status.\r\n";
return $output;
}
public function getName(): string
{
return $this->name ?: 'hubstats.txt';
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace App\Classes\Dynamic;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use App\Classes\Dynamic;
use App\Models\{Address,System};
/**
* This method will generate a nodelist for an upstream Host/RC/ZC/NC
*
* // @todo Only generate if nodes have been updated, added or removed
*/
class NodelistSegment extends Dynamic
{
private const LOGKEY = 'DNL';
private string $name = '';
private Address $our_address;
private Carbon $now;
public function __construct(Address $ao,Collection $arg)
{
$this->our_address = our_address($ao->zone->domain)->first();
$this->now = Carbon::now();
$this->name = sprintf('z%dn%d.%d',
$this->our_address->zone->zone_id,
$this->our_address->host_id,
$this->now->format('z'),
);
Log::debug(sprintf('%s:- Generating Nodelist for [%s] from [%s] as [%s] with arguments',self::LOGKEY,$ao->ftn,$this->our_address->ftn,$this->our_address->role_name),['args'=>$arg]);
}
public function __toString(): string
{
return $this->crc($this->generate($this->our_address));
}
private function crc(string $text): string
{
return sprintf(";A %s Nodelist for %s -- Day number %d : %s\r\n",
$this->our_address->role_name,
$this->now->format('l, M d, Y'),
$this->now->format('z'),
crc16($text)
).$text;
}
private function flags(Address $ao): Collection
{
$result = collect();
if (($ao->system->pollmode === TRUE) || in_array($ao->role_name,['ZC','RC','NC','HC']))
$result->push('CM');
if ($ao->system->address) {
$result->push(sprintf('INA:%s',$ao->system->address));
if (($x=$ao->system->mailers->pluck('name')->search('BINKP')) !== FALSE)
$result->push(sprintf('IBN%s',(($y=$ao->system->mailers->get($x)->pivot->port) !== 24554) ? ':'.$y : ''));
if (($x=$ao->system->mailers->pluck('name')->search('EMSI')) !== FALSE)
$result->push(sprintf('ITN%s',(($y=$ao->system->mailers->get($x)->pivot->port) !== 23) ? ':'.$y : ''));
}
return $result;
}
private function entry(Address $ao): string
{
$format = '%s,%d,%s,%s,%s,%s,300';
$prefix = '';
$address = $ao->node_id;
if ((! $ao->system->address) || $ao->isPrivate) {
$prefix = 'Pvt';
} elseif ($ao->isHold) {
$prefix = 'Hold';
} elseif ($ao->isDown) {
$prefix = 'Down';
} else
switch ($ao->role_id) {
case Address::NODE_ZC:
$prefix = 'Zone';
$address = $ao->zone->zone_id;
break;
case Address::NODE_RC:
$prefix = 'Region';
$address = $ao->region_id;
break;
case Address::NODE_NC:
$prefix = 'Host';
$address = $ao->host_id;
break;
case Address::NODE_HC:
$prefix = 'Hub';
break;
case Address::NODE_NN:
break;
case Address::NODE_POINT:
throw new \Exception(sprintf('We have no method to include points in the nodelist [%s]',$ao->ftn));
default:
throw new \Exception(sprintf('Unknown role [%d] for [%s]',$ao->role,$ao->ftn));
}
return sprintf($format,
$prefix,
$address,
$this->format($ao->system->name),
$this->format($ao->system->location),
$this->format($ao->system->sysop),
$ao->system->phone ?: '-Unpublished-',
).(($x=$this->flags($ao)->join(',')) ? sprintf(',%s',$x) : '');
}
private function format(string $string): string
{
return str_replace(' ','_',str_replace(',','',$string));
}
private function generate(Address $ao): string
{
$result = collect();
$so = System::createUnknownSystem();
$result->push($this->entry($ao));
foreach ($ao->children() as $oo) {
// We dont include points in the ndoelist
if ($oo->point_id)
continue;
// We exclude discovered systems
if ($oo->system_id == $so->id)
continue;
$result->push($this->generate($oo) ?: $this->entry($oo));
}
return $result->join("\n");
}
public function getName(): string
{
return $this->name;
}
}

View File

@ -11,7 +11,7 @@ abstract class FTN
public function __get(string $key) public function __get(string $key)
{ {
switch ($key) { switch ($key) {
case 'fftn': case 'fftn_t':
return sprintf('%d:%d/%d.%d', return sprintf('%d:%d/%d.%d',
$this->fz, $this->fz,
$this->fn, $this->fn,
@ -19,7 +19,7 @@ abstract class FTN
$this->fp, $this->fp,
).($this->zone ? sprintf('@%s',$this->zone->domain->name) : ''); ).($this->zone ? sprintf('@%s',$this->zone->domain->name) : '');
case 'tftn': case 'tftn_t':
return sprintf('%d:%d/%d.%d', return sprintf('%d:%d/%d.%d',
$this->tz, $this->tz,
$this->tn, $this->tn,
@ -27,30 +27,16 @@ abstract class FTN
$this->tp, $this->tp,
).($this->zone ? sprintf('@%s',$this->zone->domain->name) : ''); ).($this->zone ? sprintf('@%s',$this->zone->domain->name) : '');
case 'fftn_o': case 'fftn':
return Address::findFTN($this->fftn); return Address::findFTN($this->fftn_t);
case 'tftn_o': case 'tftn':
return Address::findFTN($this->tftn); return Address::findFTN($this->tftn_t);
default: default:
throw new \Exception('Unknown key: '.$key); throw new \Exception('Unknown key: '.$key);
} }
} }
/**
* Determine if a line is a kludge line.
*
* @param string $kludge
* @param string $string
* @return string
*/
protected function kludge(string $kludge,string $string)
{
return (preg_match("/^{$kludge}/",$string))
? chop(preg_replace("/^{$kludge}/",'',$string),"\r")
: FALSE;
}
/** /**
* This function creates our unpack header * This function creates our unpack header
* *

File diff suppressed because it is too large Load Diff

View File

@ -5,23 +5,38 @@ namespace App\Classes\FTN;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
use App\Classes\FTN as FTNBase; use App\Classes\FTN as FTNBase;
use App\Models\{Address,Software,System,Zone}; use App\Exceptions\InvalidPacketException;
use App\Models\{Address,Domain,Echomail,Netmail,Software,System,Zone};
use App\Notifications\Netmails\EchomailBadAddress;
/** /**
* Represents the structure of a message bundle * Represents a Fidonet Packet, that contains an array of messages.
*
* Thus this object is iterable as an array of Echomail::class or Netmail::class.
*/ */
class Packet extends FTNBase implements \Iterator, \Countable abstract class Packet extends FTNBase implements \Iterator, \Countable
{ {
private const LOGKEY = 'PKT'; private const LOGKEY = 'PKT';
private const BLOCKSIZE = 1024;
protected const PACKED_MSG_LEAD = "\02\00"; protected const PACKED_MSG_LEAD = "\02\00";
protected const PACKED_END = "\00\00";
public const MSG_TYPE2 = 1<<0;
public const MSG_TYPE4 = 1<<2;
// @todo Rename this regex to something more descriptive, ie: FILENAME_REGEX
public const regex = '([[:xdigit:]]{4})(?:-(\d{4,10}))?-(.+)';
/**
* Packet types we support, in specific order for auto-detection to work
*
* @var string[]
*/
public const PACKET_TYPES = [ public const PACKET_TYPES = [
'2.2' => FTNBase\Packet\FSC45::class, '2.2' => FTNBase\Packet\FSC45::class,
'2+' => FTNBase\Packet\FSC48::class, '2+' => FTNBase\Packet\FSC48::class,
@ -30,127 +45,17 @@ class Packet extends FTNBase implements \Iterator, \Countable
]; ];
protected array $header; // Packet Header protected array $header; // Packet Header
protected ?string $name; // Packet name protected ?string $name = NULL; // Packet name
public File $file; // Packet filename public File $file; // Packet filename
public Collection $messages; // Messages in the Packet protected Address $fftn_p; // Address the packet is from (when packing messages)
protected Address $tftn_p; // Address the packet is to (when packing messages)
protected Collection $messages; // Messages in the Packet
public Collection $errors; // Messages that fail validation public Collection $errors; // Messages that fail validation
public bool $use_cache = FALSE; // Use a cache for messages.
protected int $index; // Our array index protected int $index; // Our array index
protected $pass_p = NULL; // Overwrite the packet password (when packing messages)
/** /* ABSTRACT */
* @param string|null $header
* @throws \Exception
*/
public function __construct(string $header=NULL)
{
$this->messages = collect();
$this->errors = collect();
$this->domain = NULL;
$this->name = NULL;
if ($header)
$this->header = unpack(self::unpackheader(static::HEADER),$header);
}
/**
* @throws \Exception
*/
public function __get($key)
{
switch ($key) {
// From Addresses
case 'fz': return Arr::get($this->header,'ozone');
case 'fn': return Arr::get($this->header,'onet');
case 'ff': return Arr::get($this->header,'onode');
case 'fp': return Arr::get($this->header,'opoint');
case 'fd': return rtrim(Arr::get($this->header,'odomain',"\x00"));
// To Addresses
case 'tz': return Arr::get($this->header,'dzone');
case 'tn': return Arr::get($this->header,'dnet');
case 'tf': return Arr::get($this->header,'dnode');
case 'tp': return Arr::get($this->header,'dpoint');
case 'td': return rtrim(Arr::get($this->header,'ddomain',"\x00"));
case 'date':
return Carbon::create(
Arr::get($this->header,'y'),
Arr::get($this->header,'m')+1,
Arr::get($this->header,'d'),
Arr::get($this->header,'H'),
Arr::get($this->header,'M'),
Arr::get($this->header,'S')
);
case 'password':
return rtrim(Arr::get($this->header,$key),"\x00");
case 'fftn':
case 'fftn_o':
case 'tftn':
case 'tftn_o':
return parent::__get($key);
case 'software':
$code = Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo');
Software::unguard();
$o = Software::singleOrNew(['code'=>$code,'type'=>Software::SOFTWARE_TOSSER]);
Software::reguard();
return $o;
case 'software_ver':
return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min'));
case 'capability':
// This needs to be defined in child classes, since not all children have it
return NULL;
// Packet Type
case 'type':
return static::TYPE;
// Packet name:
case 'name':
return $this->{$key} ?: sprintf('%08x',timew());
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* Return the packet
*
* @return string
* @throws \Exception
*/
public function __toString(): string
{
$return = $this->header();
foreach ($this->messages as $o) {
if ($o->packed)
$return .= self::PACKED_MSG_LEAD.$o;
}
$return .= "\00\00";
return $return;
}
/* STATIC */
/**
* Site of the packet header
*
* @return int
*/
public static function header_len(): int
{
return collect(static::HEADER)->sum(function($item) { return Arr::get($item,2); });
}
/** /**
* This function is intended to be implemented in child classes to test if the packet * This function is intended to be implemented in child classes to test if the packet
@ -160,23 +65,32 @@ class Packet extends FTNBase implements \Iterator, \Countable
* @param string $header * @param string $header
* @return bool * @return bool
*/ */
public static function is_type(string $header): bool abstract public static function is_type(string $header): bool;
abstract protected function header(): string;
/* STATIC */
/**
* Size of the packet header
*
* @return int
*/
public static function header_len(): int
{ {
return FALSE; return collect(static::HEADER)->sum(function($item) { return Arr::get($item,2); });
} }
/** /**
* Process a packet file * Process a packet file
* *
* @param mixed $f * @param mixed $f File handler returning packet data
* @param string $name * @param string $name
* @param int $size * @param int $size
* @param System|null $system * @param Domain|null $domain
* @param bool $use_cache
* @return Packet * @return Packet
* @throws InvalidPacketException * @throws InvalidPacketException
*/ */
public static function process(mixed $f,string $name,int $size,System $system=NULL,bool $use_cache=FALSE): self public static function process(mixed $f,string $name,int $size,Domain $domain=NULL): self
{ {
Log::debug(sprintf('%s:+ Opening Packet [%s] with size [%d]',self::LOGKEY,$name,$size)); Log::debug(sprintf('%s:+ Opening Packet [%s] with size [%d]',self::LOGKEY,$name,$size));
@ -207,123 +121,185 @@ class Packet extends FTNBase implements \Iterator, \Countable
if (! $o) if (! $o)
throw new InvalidPacketException('Cannot determine type of packet.'); throw new InvalidPacketException('Cannot determine type of packet.');
$o->use_cache = $use_cache;
$o->name = $name; $o->name = $name;
$x = fread($f,2); $x = fread($f,2);
if (strlen($x) === 2) {
// End of Packet? // End of Packet?
if ((strlen($x) === 2) && ($x === "\00\00")) if ($x === "\00\00")
return $o; return $o;
// Messages start with self::PACKED_MSG_LEAD // Messages start with self::PACKED_MSG_LEAD
if ((strlen($x) === 2) && ($x !== self::PACKED_MSG_LEAD)) elseif ($x !== self::PACKED_MSG_LEAD)
throw new InvalidPacketException('Not a valid packet: '.bin2hex($x)); throw new InvalidPacketException('Not a valid packet: '.bin2hex($x));
// No message attached // No message attached
else if (! strlen($x)) } else
throw new InvalidPacketException('No message in packet: '.bin2hex($x)); throw new InvalidPacketException('Not a valid packet, not EOP or SOM:'.bin2hex($x));
$o->zone = $system?->zones->firstWhere('zone_id',$o->fz); Log::info(sprintf('%s:- Packet [%s] is a [%s] packet, dated [%s]',self::LOGKEY,$o->name,get_class($o),$o->date));
// If zone is null, we'll take the zone from the packet // Work out the packet zone
if (! $o->zone) if ($o->fz && ($o->fd || $domain)) {
$o->zone = Zone::where('zone_id',$o->fz)->where('default',TRUE)->single(); $o->zone = Zone::select('zones.*')
->join('domains',['domains.id'=>'zones.domain_id'])
->where('zone_id',$o->fz)
->where('name',$o->fd ?: $domain->name)
->single();
}
$buf_ptr = 0; // If zone is not set, then we need to use a default zone - the messages may not be from this zone.
$message = ''; if (empty($o->zone)) {
$readbuf = ''; Log::alert(sprintf('%s:! We couldnt work out the packet zone, so we have fallen back to the default for [%d]',self::LOGKEY,$o->fz));
$last = '';
while ($buf_ptr || (! feof($f) && ($readbuf=fread($f,self::BLOCKSIZE)))) { $o->zone = Zone::where('zone_id',$o->fz)
if (! $buf_ptr) ->where('default',TRUE)
->singleOrFail();
}
$message = ''; // Current message we are building
$msgbuf = '';
$leader = Message::header_len()+strlen(self::PACKED_MSG_LEAD);
// We loop through reading from the buffer, to find our end of message tag
while ((! feof($f) && ($readbuf=fread($f,$leader)))) {
$read_ptr = ftell($f); $read_ptr = ftell($f);
$msgbuf .= $readbuf;
// Packed messages are Message::HEADER_LEN, prefixed with self::PACKED_MSG_LEAD // See if we have our EOM/EOP marker
if (strlen($message) < (Message::HEADER_LEN+strlen(self::PACKED_MSG_LEAD))) { if ((($end=strpos($msgbuf,"\x00".self::PACKED_MSG_LEAD,$leader)) !== FALSE)
$addchars = (Message::HEADER_LEN+strlen(self::PACKED_MSG_LEAD))-strlen($message); || (($end=strpos($msgbuf,"\x00".self::PACKED_END,$leader)) !== FALSE))
$message .= substr($readbuf,$buf_ptr,$addchars); {
$buf_ptr += $addchars; // Parse our message
$o->parseMessage(substr($msgbuf,0,$end));
// If our buffer wasnt big enough... $msgbuf = substr($msgbuf,$end+3);
if ($buf_ptr >= strlen($readbuf)) {
$buf_ptr = 0;
continue; continue;
}
}
// Take 2 chars from the buffer and check if we have our end packet signature // If we have more to read
if ($last && ($buf_ptr === 0)) { } elseif ($read_ptr < $size) {
$last .= substr($readbuf,0,2);
if (($end=strpos($last,"\x00".self::PACKED_MSG_LEAD,$buf_ptr)) !== FALSE) {
$o->parseMessage(substr($message,0,$end-2));
$last = '';
$message = '';
$buf_ptr = 1+$end;
// Loop to rebuild our header for the next message
continue; continue;
} }
$last = ''; // If we get here
throw new InvalidPacketException(sprintf('Cannot determine END of message/packet: %s|%s',get_class($o),hex_dump($message)));;
} }
if (($end=strpos($readbuf,"\x00".self::PACKED_MSG_LEAD,$buf_ptr)) === FALSE) { if ($msgbuf)
// Just in case our packet break is at the end of the buffer throw new InvalidPacketException(sprintf('Unprocessed data in packet: %s|%s',get_class($o),hex_dump($msgbuf)));
$last = substr($readbuf,-2);
if ((str_contains($last,"\x00")) && ($size-$read_ptr > 2)) {
$message .= substr($readbuf,$buf_ptr);
$buf_ptr = 0;
continue;
}
$last = '';
$end = strpos($readbuf,"\x00\x00\x00",$buf_ptr);
}
// See if we have found the end of the packet, if not read more.
if ($end === FALSE && ($read_ptr < $size)) {
$message .= substr($readbuf,$buf_ptr);
$buf_ptr = 0;
continue;
} else {
$message .= substr($readbuf,$buf_ptr,$end-$buf_ptr);
$buf_ptr = $end+3;
if ($buf_ptr >= strlen($readbuf))
$buf_ptr = 0;
}
// Look for the next message
$o->parseMessage($message);
$message = '';
}
// If our message is still set, then we have an unprocessed message
if ($message)
$o->parseMessage($message);
return $o; return $o;
} }
/** /**
* Location of the version * @param string|null $header
* * @throws \Exception
* @return int
*/ */
public static function version_offset(): int public function __construct(string $header=NULL)
{ {
return Arr::get(collect(static::HEADER)->get('type'),0); $this->messages = collect();
$this->errors = collect();
if ($header)
$this->header = unpack(self::unpackheader(static::HEADER),$header);
} }
public static function version_offset_len(): int /**
* @throws \Exception
*/
public function __get($key)
{ {
return Arr::get(collect(static::HEADER)->get('type'),2); //Log::debug(sprintf('%s:/ Requesting key for Packet::class [%s]',self::LOGKEY,$key));
switch ($key) {
// From Addresses
case 'fz': return Arr::get($this->header,'ozone');
case 'fn': return Arr::get($this->header,'onet');
case 'ff': return Arr::get($this->header,'onode');
case 'fp': return Arr::get($this->header,'opoint');
case 'fd': return rtrim(Arr::get($this->header,'odomain',"\x00"));
// To Addresses
case 'tz': return Arr::get($this->header,'dzone');
case 'tn': return Arr::get($this->header,'dnet');
case 'tf': return Arr::get($this->header,'dnode');
case 'tp': return Arr::get($this->header,'dpoint');
case 'td': return rtrim(Arr::get($this->header,'ddomain',"\x00"));
case 'date':
return Carbon::create(
Arr::get($this->header,'y'),
Arr::get($this->header,'m')+1,
Arr::get($this->header,'d'),
Arr::get($this->header,'H'),
Arr::get($this->header,'M'),
Arr::get($this->header,'S')
);
case 'password':
return rtrim(Arr::get($this->header,$key),"\x00");
case 'fftn_t':
case 'fftn':
case 'tftn_t':
case 'tftn':
return parent::__get($key);
case 'product':
return Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo');
case 'software':
Software::unguard();
$o = Software::singleOrNew(['code'=>$this->product,'type'=>Software::SOFTWARE_TOSSER]);
Software::reguard();
return $o;
case 'software_ver':
return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min'));
case 'capability':
// This needs to be defined in child classes, since not all children have it
return NULL;
// Packet Type
case 'type':
return static::TYPE;
// Packet name:
case 'name':
return $this->{$key} ?: sprintf('%08x',timew());
case 'messages':
return $this->{$key};
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* Return the packet
*
* @return string
* @throws \Exception
*/
public function __toString(): string
{
if (empty($this->messages))
throw new InvalidPacketException('Refusing to make an empty packet');
if (empty($this->tftn_p) || empty($this->fftn_p))
throw new InvalidPacketException('Cannot generate a packet without a destination address');
$return = $this->header();
foreach ($this->messages as $o)
$return .= self::PACKED_MSG_LEAD.$o->packet($this->tftn_p);
$return .= "\00\00";
return $return;
} }
/* INTERFACE */ /* INTERFACE */
@ -336,14 +312,14 @@ class Packet extends FTNBase implements \Iterator, \Countable
return $this->messages->count(); return $this->messages->count();
} }
public function current(): Message public function current(): Echomail|Netmail
{ {
return $this->use_cache ? unserialize(Cache::pull($this->key())) : $this->messages->get($this->index); return $this->messages->get($this->index);
} }
public function key(): mixed public function key(): mixed
{ {
return $this->use_cache ? $this->messages->get($this->index) : $this->index; return $this->index;
} }
public function next(): void public function next(): void
@ -358,7 +334,7 @@ class Packet extends FTNBase implements \Iterator, \Countable
public function valid(): bool public function valid(): bool
{ {
return (! is_null($this->key())) && ($this->use_cache ? Cache::has($this->key()) : $this->messages->has($this->key())); return (! is_null($this->key())) && $this->messages->has($this->key());
} }
/* METHODS */ /* METHODS */
@ -369,6 +345,7 @@ class Packet extends FTNBase implements \Iterator, \Countable
* @param Address $oo * @param Address $oo
* @param Address $o * @param Address $o
* @param string|null $passwd Override the password used in the packet * @param string|null $passwd Override the password used in the packet
* @deprecated Use Packet::generate(), which should generate a packet of the right type
*/ */
public function addressHeader(Address $oo,Address $o,string $passwd=NULL): void public function addressHeader(Address $oo,Address $o,string $passwd=NULL): void
{ {
@ -394,7 +371,7 @@ class Packet extends FTNBase implements \Iterator, \Countable
'H' => $date->format('H'), // Hour 'H' => $date->format('H'), // Hour
'M' => $date->format('i'), // Minute 'M' => $date->format('i'), // Minute
'S' => $date->format('s'), // Second 'S' => $date->format('s'), // Second
'password' => (! is_null($passwd)) ? $passwd : $o->session('pktpass'), // Packet Password 'password' => strtoupper((! is_null($passwd)) ? $passwd : $o->session('pktpass')), // Packet Password
]; ];
} }
@ -402,12 +379,38 @@ class Packet extends FTNBase implements \Iterator, \Countable
* Add a message to this packet * Add a message to this packet
* *
* @param Message $o * @param Message $o
* @deprecated No longer used when Address::class is updated
*/ */
public function addMail(Message $o): void public function addMail(Message $o): void
{ {
$this->messages->push($o); $this->messages->push($o);
} }
public function for(Address $ao): self
{
$this->tftn_p = $ao;
$this->fftn_p = our_address($ao);
return $this;
}
/**
* Generate a packet
*
* @return string
*/
public function generate(): string
{
return (string)$this;
}
public function mail(Collection $msgs): self
{
$this->messages = $msgs;
return $this;
}
/** /**
* Parse a message in a mail packet * Parse a message in a mail packet
* *
@ -416,113 +419,100 @@ class Packet extends FTNBase implements \Iterator, \Countable
*/ */
private function parseMessage(string $message): void private function parseMessage(string $message): void
{ {
Log::info(sprintf('%s:Processing message [%d] bytes',self::LOGKEY,strlen($message))); Log::info(sprintf('%s:+ Processing packet message [%d] bytes',self::LOGKEY,strlen($message)));
$msg = Message::parseMessage($message,$this->zone); $msg = Message::parseMessage($message,$this->zone);
// If the message is invalid, we'll ignore it // If the message is invalid, we'll ignore it
if ($msg->errors) { if ($msg->errors->count()) {
Log::info(sprintf('%s:- Message [%s] has errors',self::LOGKEY,$msg->msgid)); Log::info(sprintf('%s:- Message [%s] has [%d] errors',self::LOGKEY,$msg->msgid ?: 'No ID',$msg->errors->count()));
// If the from address doenst exist, we'll create a new entry
if ($msg->errors->messages()->has('to') && $msg->tzone) {
try {
// @todo Need to work out the correct region for the host_id
Address::unguard();
$ao = Address::firstOrNew([
'zone_id' => $msg->tzone->id,
//'region_id' => 0,
'host_id' => $msg->tn,
'node_id' => $msg->tf,
'point_id' => $msg->tp,
'active' => TRUE,
]);
Address::reguard();
if (is_null($ao->region_id)) // If the messages is not for the right zone, we'll ignore it
$ao->region_id = $ao->host_id; if ($msg->errors->has('invalid-zone')) {
Log::alert(sprintf('%s:! Message [%s] is from an invalid zone [%s], packet is from [%s] - ignoring it',self::LOGKEY,$msg->msgid,$msg->fftn->zone->zone_id,$this->fftn->zone->zone_id));
if (! $msg->kludges->get('RESCANNED'))
Notification::route('netmail',$this->fftn)->notify(new EchomailBadAddress($msg));
} catch (\Exception $e) {
Log::error(sprintf('%s:! Error finding/creating TO address [%s] for message',self::LOGKEY,$msg->tboss),['error'=>$e->getMessage()]);
$this->errors->push($msg);
return; return;
} }
$ao->role = Address::NODE_UNKNOWN; // If the $msg->fftn doesnt exist, we'll need to create it
if ($msg->errors->has('from') && $this->fftn && $this->fftn->zone_id) {
Log::debug(sprintf('%s:^ From address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_fftn')));
System::unguard(); $ao = Address::findFTN($msg->set->get('set_fftn'),TRUE);
$so = System::firstOrCreate([
'name' => 'Discovered System',
'sysop' => 'Unknown',
'location' => '',
'active' => TRUE,
]);
System::reguard();
if ($so->id !== 443)
Log::alert(sprintf('%s:? Just created Discovered System for MSGID [%s] A',self::LOGKEY,$msg->msgid));
$so->addresses()->save($ao); if ($ao?->exists && ($ao->zone?->domain_id !== $this->fftn->zone->domain_id)) {
Log::alert(sprintf('%s:! From address [%s] domain [%d] doesnt match packet domain [%d]?',self::LOGKEY,$msg->set->get('set_fftn'),$ao->zone?->domain_id,$this->fftn->zone->domain_id));
Log::alert(sprintf('%s: - To FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->tboss,$ao->id));
}
if ($msg->errors->messages()->has('from') && $msg->tzone) {
try {
// @todo Need to work out the correct region for the host_id
Address::unguard();
$ao = Address::firstOrNew([
'zone_id' => $msg->fzone->id,
//'region_id' => 0,
'host_id' => $msg->fn,
'node_id' => $msg->ff,
'point_id' => $msg->fp,
'active'=> TRUE,
]);
Address::reguard();
if (is_null($ao->region_id))
$ao->region_id = $ao->host_id;
} catch (\Exception $e) {
Log::error(sprintf('%s:! Error finding/creating FROM address [%s] for message',self::LOGKEY,$msg->fboss),['error'=>$e->getMessage()]);
$this->errors->push($msg);
return; return;
} }
$ao->role = Address::NODE_UNKNOWN; if (! $ao) {
$so = System::createUnknownSystem();
System::unguard(); $ao = Address::createFTN($msg->set->get('set_fftn'),$so);
$so = System::firstOrCreate([
'name' => 'Discovered System',
'sysop' => 'Unknown',
'location' => '',
'active' => TRUE,
]);
System::reguard();
if ($so->id !== 443)
Log::alert(sprintf('%s:? Just created Discovered System for MSGID [%s] B',self::LOGKEY,$msg->msgid));
$so->addresses()->save($ao);
Log::alert(sprintf('%s: - From FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->fboss,$ao->id));
} }
if ($msg->errors->messages()->has('user_from') || $msg->errors->messages()->has('user_to')) { $msg->fftn_id = $ao->id;
Log::error(sprintf('%s:! Skipping message [%s] due to errors (%s)...',self::LOGKEY,$msg->msgid,join(',',$msg->errors->messages()->keys()))); Log::alert(sprintf('%s:- From FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_fftn'),$ao->id));
$this->errors->push($msg); }
// If the $msg->tftn doesnt exist, we'll need to create it
if ($msg->errors->has('to') && $this->tftn && $this->tftn->zone_id) {
Log::debug(sprintf('%s:^ To address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_tftn')));
$ao = Address::findFTN($msg->set->get('set_tftn'),TRUE);
if ($ao?->exists && ($ao->zone?->domain_id !== $this->tftn->zone->domain_id)) {
Log::alert(sprintf('%s:! To address [%s] domain [%d] doesnt match packet domain [%d]?',self::LOGKEY,$msg->set->get('set_tftn'),$ao->zone?->domain_id,$this->fftn->zone->domain_id));
return;
}
if (! $ao) {
$so = System::createUnknownSystem();
$ao = Address::createFTN($msg->set->get('set_fftn'),$so);
}
$msg->tftn_id = $ao->id;
Log::alert(sprintf('%s:- To FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_tftn'),$ao->id));
}
// If there is no fftn, then its from a system that we dont know about
if (! $this->fftn) {
Log::alert(sprintf('%s:! No further message processing, packet is from a system we dont know about [%s]',self::LOGKEY,$this->fftn_t));
$this->messages->push($msg);
return; return;
} }
} }
if ($this->use_cache) { // @todo If the message from domain (eg: $msg->fftn->zone->domain) is different to the packet address domain ($pkt->fftn->zone->domain), we'll skip this message
$key = urlencode($msg->msgid ?: sprintf('%s %s',$msg->fftn,Carbon::now()->timestamp)); Log::debug(sprintf('%s:^ Message [%s] - Packet from domain [%d], Message domain [%d]',self::LOGKEY,$msg->msgid,$this->fftn->zone->domain_id,$msg->fftn->zone->domain_id));
if (! Cache::forever($key,serialize($msg)))
throw new \Exception(sprintf('Caching failed for key [%s]?',$key));
$this->messages->push($key);
} else {
$this->messages->push($msg); $this->messages->push($msg);
} }
/**
* Overwrite the packet password
*
* @param string|null $password
* @return self
*/
public function password(string $password=NULL): self
{
if ($password && (strlen($password) < 9))
$this->pass_p = $password;
return $this;
}
/** @deprecated Is this used? */
public function pluck(string $key): Collection
{
throw new \Exception(sprintf('%s:! This function is deprecated - [%s]',self::LOGKEY,$key));
return $this->messages->pluck($key);
} }
} }

View File

@ -30,14 +30,14 @@ final class FSC39 extends Packet
'dnet' => [0x16,'v',2], // Dest Net 'dnet' => [0x16,'v',2], // Dest Net
'prodcode-lo' => [0x18,'C',1], // Product Code 'prodcode-lo' => [0x18,'C',1], // Product Code
'prodrev-maj' => [0x19,'C',1], // Product Version Major 'prodrev-maj' => [0x19,'C',1], // Product Version Major
'password' => [0x1a,'a8',8], // Packet Password 'password' => [0x1a,'a8',8], // Packet Password - http://ftsc.org/docs/fsc-0039.004 packet passwords are A-Z,0-9
'ozone' => [0x22,'v',2], // Orig Zone 'ozone' => [0x22,'v',2], // Orig Zone
'dzone' => [0x24,'v',2], // Dest Zone 'dzone' => [0x24,'v',2], // Dest Zone
'reserved' => [0x26,'a2',2], // Reserved 'reserved' => [0x26,'a2',2], // Reserved
'capvalid' => [0x28,'n',2], // fsc-0039.004 (copy of 0x2c) 'capvalid' => [0x28,'n',2], // fsc-0039.004 (copy of 0x2c)
'prodcode-hi' => [0x2a,'C',1], // Product Code Hi 'prodcode-hi' => [0x2a,'C',1], // Product Code Hi
'prodrev-min' => [0x2b,'C',1], // Product Version Minor 'prodrev-min' => [0x2b,'C',1], // Product Version Minor
'capword' => [0x2c,'v',2], // Capability Word 'capword' => [0x2c,'v',2], // Capability Word fsc-0039.004/fsc-0048.002
'dozone' => [0x2e,'v',2], // Orig Zone 'dozone' => [0x2e,'v',2], // Orig Zone
'ddzone' => [0x30,'v',2], // Dest Zone 'ddzone' => [0x30,'v',2], // Dest Zone
'opoint' => [0x32,'v',2], // Orig Point 'opoint' => [0x32,'v',2], // Orig Point
@ -46,6 +46,7 @@ final class FSC39 extends Packet
]; ];
public const TYPE = '2e'; public const TYPE = '2e';
public const VERS = self::MSG_TYPE2; //|self::MSG_TYPE4;
public function __get($key) public function __get($key)
{ {
@ -63,34 +64,36 @@ final class FSC39 extends Packet
*/ */
protected function header(): string protected function header(): string
{ {
$oldest = $this->messages->sortBy('datetime')->last();
try { try {
return pack(collect(self::HEADER)->pluck(1)->join(''), return pack(collect(self::HEADER)->pluck(1)->join(''),
$this->ff, // Orig Node $this->fftn_p->node_id, // Orig Node
$this->tf, // Dest Node $this->tftn_p->node_id, // Dest Node
Arr::get($this->header,'y'), // Year $oldest->datetime->format('Y'), // Year
Arr::get($this->header,'m'), // Month $oldest->datetime->format('m')-1, // Month
Arr::get($this->header,'d'), // Day $oldest->datetime->format('d'), // Day
Arr::get($this->header,'H'), // Hour $oldest->datetime->format('H'), // Hour
Arr::get($this->header,'M'), // Minute $oldest->datetime->format('i'), // Minute
Arr::get($this->header,'S'), // Second $oldest->datetime->format('s'), // Second
0, // Baud 0, // Baud
2, // Packet Version (should be 2) 2, // Packet Version (should be 2)
$this->fn, // Orig Net $this->fftn_p->host_id, // Orig Net
$this->tn, // Dest Net $this->tftn_p->host_id, // Dest Net
(Setup::PRODUCT_ID & 0xff), // Product Code Lo (Setup::PRODUCT_ID & 0xff), // Product Code Lo
Setup::PRODUCT_VERSION_MAJ, // Product Version Major Setup::PRODUCT_VERSION_MAJ, // Product Version Major
$this->password, // Packet Password $this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password
$this->fz, // Orig Zone $this->fftn_p->zone->zone_id, // Orig Zone
$this->tz, // Dest Zone $this->tftn_p->zone->zone_id, // Dest Zone
'', // Reserved '', // Reserved
Arr::get($this->header,'capvalid',1<<0), // fsc-0039.004 (copy of 0x2c) static::VERS, // fsc-0039.004 (copy of 0x2c)
((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi ((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi
Setup::PRODUCT_VERSION_MIN, // Product Version Minor Setup::PRODUCT_VERSION_MIN, // Product Version Minor
Arr::get($this->header,'capword',1<<0), // Capability Word static::VERS, // Capability Word
$this->fz, // Orig Zone $this->fftn_p->zone->zone_id, // Orig Zone
$this->tz, // Dest Zone $this->tftn_p->zone->zone_id, // Dest Zone
$this->fp, // Orig Point $this->fftn_p->point_id, // Orig Point
$this->tp, // Dest Point $this->tftn_p->point_id, // Dest Point
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
); );

View File

@ -37,6 +37,20 @@ final class FSC45 extends Packet
public const TYPE = '2.2'; public const TYPE = '2.2';
public function __get($key)
{
switch ($key) {
case 'product':
return Arr::get($this->header,'prodcode');
case 'software_ver':
return Arr::get($this->header,'prodrev-maj');
default:
return parent::__get($key);
}
}
/** /**
* Create our message packet header * Create our message packet header
*/ */
@ -44,22 +58,22 @@ final class FSC45 extends Packet
{ {
try { try {
return pack(collect(self::HEADER)->pluck(1)->join(''), return pack(collect(self::HEADER)->pluck(1)->join(''),
$this->ff, // Orig Node $this->fftn_p->node_id, // Orig Node
$this->tf, // Dest Node $this->tftn_p->node_id, // Dest Node
$this->fp, // Orig Point $this->fftn_p->point_id, // Orig Point
$this->tp, // Dest Point $this->tftn_p->point_id, // Dest Point
'', // Reserved '', // Reserved
2, // Sub Version (should be 2) 2, // Sub Version (should be 2)
2, // Packet Version (should be 2) 2, // Packet Version (should be 2)
$this->fn, // Orig Net $this->fftn_p->host_id, // Orig Net
$this->tn, // Dest Net $this->tftn_p->host_id, // Dest Net
(Setup::PRODUCT_ID & 0xff), // Product Code (Setup::PRODUCT_ID & 0xff), // Product Code
Setup::PRODUCT_VERSION_MAJ, // Product Version Setup::PRODUCT_VERSION_MAJ, // Product Version
$this->password, // Packet Password $this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password
$this->fz, // Orig Zone $this->fftn_p->zone->zone_id, // Orig Zone
$this->tz, // Dest Zone $this->tftn_p->zone->zone_id, // Dest Zone
$this->fd, // Orig Domain $this->fftn_p->zone->domain->name, // Orig Domain
$this->td, // Dest Domain $this->tftn_p->zone->domain->name, // Dest Domain
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
); );

View File

@ -11,6 +11,7 @@ use App\Models\Setup;
* FSC-0048 http://ftsc.org/docs/fsc-0048.002 * FSC-0048 http://ftsc.org/docs/fsc-0048.002
* *
* Commonly known as Type 2+ packets, based on FSC-0039 with improved support for FTS-0001 * Commonly known as Type 2+ packets, based on FSC-0039 with improved support for FTS-0001
* @note: These packets will be detected as FSC-0039 packets unless it is addressed to a point
*/ */
final class FSC48 extends Packet final class FSC48 extends Packet
{ {
@ -36,7 +37,7 @@ final class FSC48 extends Packet
'capvalid' => [0x28,'n',2], // fsc-0039.004 (copy of 0x2c) 'capvalid' => [0x28,'n',2], // fsc-0039.004 (copy of 0x2c)
'prodcode-hi' => [0x2a,'C',1], // Product Code Hi 'prodcode-hi' => [0x2a,'C',1], // Product Code Hi
'prodrev-min' => [0x2b,'C',1], // Product Version Minor 'prodrev-min' => [0x2b,'C',1], // Product Version Minor
'capword' => [0x2c,'v',2], // Capability Word 'capword' => [0x2c,'v',2], // Capability Word fsc-0039.004/fsc-0048.002
'dozone' => [0x2e,'v',2], // Orig Zone 'dozone' => [0x2e,'v',2], // Orig Zone
'ddzone' => [0x30,'v',2], // Dest Zone 'ddzone' => [0x30,'v',2], // Dest Zone
'opoint' => [0x32,'v',2], // Orig Point 'opoint' => [0x32,'v',2], // Orig Point
@ -45,6 +46,7 @@ final class FSC48 extends Packet
]; ];
public const TYPE = '2+'; public const TYPE = '2+';
public const VERS = self::MSG_TYPE2; //|self::MSG_TYPE4;
public function __get($key) public function __get($key)
{ {
@ -62,34 +64,36 @@ final class FSC48 extends Packet
*/ */
protected function header(): string protected function header(): string
{ {
$oldest = $this->messages->sortBy('datetime')->last();
try { try {
return pack(collect(self::HEADER)->pluck(1)->join(''), return pack(collect(self::HEADER)->pluck(1)->join(''),
$this->ff, // Orig Node $this->fftn_p->node_id, // Orig Node
$this->tf, // Dest Node $this->tftn_p->node_id, // Dest Node
Arr::get($this->header,'y'), // Year $oldest->datetime->format('Y'), // Year
Arr::get($this->header,'m'), // Month $oldest->datetime->format('m')-1, // Month
Arr::get($this->header,'d'), // Day $oldest->datetime->format('d'), // Day
Arr::get($this->header,'H'), // Hour $oldest->datetime->format('H'), // Hour
Arr::get($this->header,'M'), // Minute $oldest->datetime->format('i'), // Minute
Arr::get($this->header,'S'), // Second $oldest->datetime->format('s'), // Second
0, // Baud 0, // Baud
2, // Packet Version (should be 2) 2, // Packet Version (should be 2)
$this->fp ? 0xffff : $this->fn, // Orig Net (0xFFFF when OrigPoint != 0) $this->fftn_p->point_id ? 0xffff : $this->fftn_p->host_id, // Orig Net (0xFFFF when OrigPoint != 0)
$this->tn, // Dest Net $this->tftn_p->host_id, // Dest Net
(Setup::PRODUCT_ID & 0xff), // Product Code Lo (Setup::PRODUCT_ID & 0xff), // Product Code Lo
Setup::PRODUCT_VERSION_MAJ, // Product Version Major Setup::PRODUCT_VERSION_MAJ, // Product Version Major
$this->password, // Packet Password $this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password
$this->fz, // Orig Zone $this->fftn_p->zone->zone_id, // Orig Zone
$this->tz, // Dest Zone $this->tftn_p->zone->zone_id, // Dest Zone
$this->fp ? $this->fn : 0x00, // Aux Net $this->fftn_p->point_id ? $this->fftn_p->host_id : 0x00, // Aux Net
Arr::get($this->header,'capvalid',1<<0), // fsc-0039.004 (copy of 0x2c) static::VERS, // fsc-0039.004 (copy of 0x2c)
((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi ((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi
Setup::PRODUCT_VERSION_MIN, // Product Version Minor Setup::PRODUCT_VERSION_MIN, // Product Version Minor
Arr::get($this->header,'capword',1<<0), // Capability Word static::VERS, // Capability Word
$this->fz, // Orig Zone $this->fftn_p->zone->zone_id, // Orig Zone
$this->tz, // Dest Zone $this->tftn_p->zone->zone_id, // Dest Zone
$this->fp, // Orig Point $this->fftn_p->point_id, // Orig Point
$this->tp, // Dest Point $this->tftn_p->point_id, // Dest Point
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
); );

View File

@ -38,30 +38,46 @@ final class FTS1 extends Packet
public const TYPE = '2'; public const TYPE = '2';
public function __get($key)
{
switch ($key) {
case 'product':
return Arr::get($this->header,'prodcode');
case 'software_ver':
return 'N/A';
default:
return parent::__get($key);
}
}
/** /**
* Create our message packet header * Create our message packet header
*/ */
protected function header(): string protected function header(): string
{ {
$oldest = $this->messages->sortBy('datetime')->last();
try { try {
return pack(collect(self::HEADER)->pluck(1)->join(''), return pack(collect(self::HEADER)->pluck(1)->join(''),
$this->ff, // Orig Node $this->fftn_p->node_id, // Orig Node
$this->tf, // Dest Node $this->tftn_p->node_id, // Dest Node
Arr::get($this->header,'y'), // Year $oldest->datetime->format('Y'), // Year
Arr::get($this->header,'m'), // Month $oldest->datetime->format('m')-1, // Month
Arr::get($this->header,'d'), // Day $oldest->datetime->format('d'), // Day
Arr::get($this->header,'H'), // Hour $oldest->datetime->format('H'), // Hour
Arr::get($this->header,'M'), // Minute $oldest->datetime->format('i'), // Minute
Arr::get($this->header,'S'), // Second $oldest->datetime->format('s'), // Second
0, // Baud 0, // Baud
2, // Packet Version (should be 2) 2, // Packet Version (should be 2)
$this->fn, // Orig Net $this->fftn_p->host_id, // Orig Net
$this->tn, // Dest Net $this->tftn_p->host_id, // Dest Net
(Setup::PRODUCT_ID & 0xff), // Product Code Lo (Setup::PRODUCT_ID & 0xff), // Product Code Lo
Setup::PRODUCT_VERSION_MAJ, // Product Version Major Setup::PRODUCT_VERSION_MAJ, // Product Version Major
$this->password, // Packet Password $this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password
$this->fz, // Orig Zone $this->fftn_p->zone->zone_id, // Orig Zone
$this->tz, // Dest Zone $this->tftn_p->zone->zone_id, // Dest Zone
'', // Reserved '', // Reserved
); );

View File

@ -2,25 +2,23 @@
namespace App\Classes\FTN; namespace App\Classes\FTN;
use App\Models\Echoarea; use App\Models\{Echoarea,Echomail,Netmail};
/** /**
* Abstract class to hold the common functions for automatic responding to echomail/netmail messages * Abstract class to hold the common functions for automatic responding to echomail/netmail messages
*/ */
abstract class Process abstract class Process
{ {
public static function canProcess(string $echoarea): bool public static function canProcess(Echoarea $eao): bool
{ {
$eao = Echoarea::where('name',$echoarea)->single(); return $eao->automsgs ? TRUE : FALSE;
return $eao && $eao->automsgs;
} }
/** /**
* Return TRUE if the process class handled the message. * Return TRUE if the process class handled the message.
* *
* @param Message $msg * @param Echomail|Netmail $mo
* @return bool * @return bool
*/ */
abstract public static function handle(Message $msg): bool; abstract public static function handle(Echomail|Netmail $mo): bool;
} }

View File

@ -5,7 +5,8 @@ namespace App\Classes\FTN\Process\Echomail;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Classes\FTN\{Message,Process}; use App\Classes\FTN\Process;
use App\Models\{Echomail,Netmail};
use App\Notifications\Echomails\Test as TestNotification; use App\Notifications\Echomails\Test as TestNotification;
/** /**
@ -19,16 +20,16 @@ final class Test extends Process
private const testing = ['test','testing']; private const testing = ['test','testing'];
public static function handle(Message $msg): bool public static function handle(Echomail|Netmail $mo): bool
{ {
if (! self::canProcess($msg->echoarea) if (! self::canProcess($mo->echoarea)
|| (strtolower($msg->user_to) !== 'all') || (strtolower($mo->to) !== 'all')
|| (! in_array(strtolower($msg->subject),self::testing))) || (! in_array(strtolower($mo->subject),self::testing)))
return FALSE; return FALSE;
Log::info(sprintf('%s:- Processing TEST message from (%s) [%s] in [%s]',self::LOGKEY,$msg->user_from,$msg->fftn,$msg->echoarea)); Log::info(sprintf('%s:- Processing TEST message from (%s) [%s] in [%s]',self::LOGKEY,$mo->from,$mo->fftn->ftn,$mo->echoarea->name));
Notification::route('echomail',$msg->echoarea)->notify(new TestNotification($msg)); Notification::route('echomail',$mo->echoarea->withoutRelations())->notify(new TestNotification($mo));
return TRUE; return TRUE;
} }

View File

@ -0,0 +1,37 @@
<?php
namespace App\Classes\FTN\Process\Netmail;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process;
use App\Models\{Echomail,Netmail};
use App\Notifications\Netmails\Areafix as AreafixNotification;
use App\Notifications\Netmails\Areafix\NotConfiguredHere as AreafixNotConfiguredHereNotification;
/**
* Process messages to Ping
*
* @package App\Classes\FTN\Process
*/
final class Areafix extends Process
{
private const LOGKEY = 'RP-';
public static function handle(Echomail|Netmail $mo): bool
{
if (strtolower($mo->to) !== 'areafix')
return FALSE;
Log::info(sprintf('%s:- Processing AREAFIX message from (%s) [%s]',self::LOGKEY,$mo->from,$mo->fftn));
// If this is not a node we manage, then respond with a sorry can help you
if ($mo->fftn->system->sessions->count())
Notification::route('netmail',$mo->fftn)->notify(new AreafixNotification($mo));
else
Notification::route('netmail',$mo->fftn)->notify(new AreafixNotConfiguredHereNotification($mo));
return TRUE;
}
}

View File

@ -5,7 +5,8 @@ namespace App\Classes\FTN\Process\Netmail;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Classes\FTN\{Message,Process}; use App\Classes\FTN\Process;
use App\Models\{Echomail,Netmail};
use App\Notifications\Netmails\Ping as PingNotification; use App\Notifications\Netmails\Ping as PingNotification;
/** /**
@ -17,14 +18,14 @@ final class Ping extends Process
{ {
private const LOGKEY = 'RP-'; private const LOGKEY = 'RP-';
public static function handle(Message $msg): bool public static function handle(Echomail|Netmail $mo): bool
{ {
if (strtolower($msg->user_to) !== 'ping') if (strtolower($mo->to) !== 'ping')
return FALSE; return FALSE;
Log::info(sprintf('%s:- Processing PING message from (%s) [%s]',self::LOGKEY,$msg->user_from,$msg->fftn)); Log::info(sprintf('%s:- Processing PING message from (%s) [%s]',self::LOGKEY,$mo->from,$mo->fftn->ftn));
Notification::route('netmail',$msg->fftn_o)->notify(new PingNotification($msg)); Notification::route('netmail',$mo->fftn)->notify(new PingNotification($mo));
return TRUE; return TRUE;
} }

View File

@ -4,30 +4,30 @@ namespace App\Classes\FTN;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToWriteFile; use League\Flysystem\UnableToReadFile;
use App\Classes\FTN as FTNBase; use App\Classes\FTN as FTNBase;
use App\Exceptions\{InvalidCRCException,
InvalidPasswordException,
NodeNotSubscribedException,
NoWriteSecurityException};
use App\Exceptions\TIC\{NoFileAreaException,NotToMeException,SizeMismatchException};
use App\Models\{Address,File,Filearea,Setup}; use App\Models\{Address,File,Filearea,Setup};
use App\Traits\EncodeUTF8;
/** /**
* Class TIC * Class TIC
* Used create the structure of TIC files * This class handles the TIC files that accompany file transfers
* *
* @package App\Classes * @package App\Classes
*/ */
class Tic extends FTNBase class Tic extends FTNBase
{ {
use EncodeUTF8;
private const LOGKEY = 'FT-'; private const LOGKEY = 'FT-';
private const cast_utf8 = [
];
// Single value kludge items and whether they are required // Single value kludge items and whether they are required
// http://ftsc.org/docs/fts-5006.001 // http://ftsc.org/docs/fts-5006.001
private array $_kludge = [ private array $_kludge = [
@ -52,75 +52,79 @@ class Tic extends FTNBase
'pw' => FALSE, // Password 'pw' => FALSE, // Password
]; ];
private File $fo; private File $file;
private Filearea $area;
private Collection $values;
private Address $origin; // Should be first address in Path
private Address $from; // Should be last address in Path
private Address $to; // Should be me private Address $to; // Should be me
public function __construct() public function __construct(File $file=NULL)
{ {
$this->fo = new File; $this->file = $file ?: new File;
$this->fo->kludges = collect(); $this->file->kludges = collect();
$this->fo->set_path = collect(); $this->file->rogue_seenby = collect();
$this->fo->set_seenby = collect(); $this->file->set_path = collect();
$this->fo->rogue_path = collect(); $this->file->set_seenby = collect();
$this->fo->rogue_seenby = collect();
$this->values = collect();
} }
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
switch ($key) { switch ($key) {
case 'fo': case 'file':
return $this->{$key}; return $this->{$key};
case 'name':
return $this->file->name;
default: default:
return parent::__get($key); return parent::__get($key);
} }
} }
/** /**
* Generate a TIC file for an address * Generate the TIC file
* *
* @param Address $ao
* @param File $fo
* @return string * @return string
* @throws \Exception
*/ */
public static function generate(Address $ao,File $fo): string public function __toString(): string
{ {
$sysaddress = Setup::findOrFail(config('app.id'))->system->match($ao->zone)->first(); if (! $this->to)
throw new \Exception('No to address defined');
$sysaddress = our_address($this->to);
$result = collect(); $result = collect();
// Origin is the first address in our path // Origin is the first address in our path
$result->put('ORIGIN',$fo->path->first()->ftn3d); $result->put('ORIGIN',$this->file->path->first()->ftn3d);
$result->put('FROM',$sysaddress->ftn3d); $result->put('FROM',$sysaddress->ftn3d);
$result->put('TO',$ao->ftn3d); $result->put('TO',$this->to->ftn3d);
$result->put('FILE',$fo->name); $result->put('FILE',$this->file->name);
$result->put('SIZE',$fo->size); $result->put('SIZE',$this->file->size);
if ($fo->description) if ($this->file->description)
$result->put('DESC',$fo->description); $result->put('DESC',$this->file->description);
$result->put('AREA',$fo->filearea->name); if ($this->file->replaces)
$result->put('AREADESC',$fo->filearea->description); $result->put('REPLACES',$this->file->replaces);
if ($x=$ao->session('ticpass')) $result->put('AREA',$this->file->filearea->name);
$result->put('AREADESC',$this->file->filearea->description);
if ($x=strtoupper($this->to->session('ticpass')))
$result->put('PW',$x); $result->put('PW',$x);
$result->put('CRC',sprintf("%X",$fo->crc)); $result->put('CRC',sprintf("%X",$this->file->crc));
$out = ''; $out = '';
foreach ($result as $key=>$value) foreach ($result as $key=>$value)
$out .= sprintf("%s %s\r\n",$key,$value); $out .= sprintf("%s %s\r\n",$key,$value);
foreach ($fo->path as $o) foreach ($this->file->path as $o)
$out .= sprintf("PATH %s %s %s\r\n",$o->ftn3d,$o->pivot->datetime,$o->pivot->extra); $out .= sprintf("PATH %s %s %s\r\n",$o->ftn3d,$o->pivot->datetime,$o->pivot->extra);
foreach ($fo->seenby as $o) // Add ourself to the path:
$out .= sprintf("PATH %s %s\r\n",$sysaddress->ftn3d,Carbon::now());
foreach ($this->file->seenby as $o)
$out .= sprintf("SEENBY %s\r\n",$o->ftn3d); $out .= sprintf("SEENBY %s\r\n",$o->ftn3d);
$out .= sprintf("SEENBY %s\r\n",$sysaddress->ftn3d);
return $out; return $out;
} }
@ -131,180 +135,253 @@ class Tic extends FTNBase
*/ */
public function isNodelist(): bool public function isNodelist(): bool
{ {
return (($this->fo->nodelist_filearea_id === $this->fo->filearea->domain->filearea_id) Log::critical(sprintf('%s:D fo_nodelist_file_area [%d], fo_filearea_domain_filearea_id [%d], regex [%s] name [%s]',
&& (preg_match(str_replace(['.','?'],['\.','.'],'#^'.$this->fo->filearea->domain->nodelist_filename.'$#i'),$this->fo->name))); self::LOGKEY,
$this->file->nodelist_filearea_id,
$this->file->filearea->domain->filearea_id,
str_replace(['.','?'],['\.','[0-9]'],'#^'.$this->file->filearea->domain->nodelist_filename.'$#i'),
$this->file->name,
));
return (($this->file->nodelist_filearea_id === $this->file->filearea->domain->filearea_id)
&& (preg_match(str_replace(['.','?'],['\.','[0-9]'],'#^'.$this->file->filearea->domain->nodelist_filename.'$#i'),$this->file->name)));
} }
/** /**
* Load a TIC file from an existing filename * Load a TIC file from an existing filename
* *
* @param string $filename * @param string $filename Relative to filesystem
* @return void * @return File
* @throws FileNotFoundException * @throws FileNotFoundException
* @throws InvalidCRCException
* @throws InvalidPasswordException
* @throws NoFileAreaException
* @throws NoWriteSecurityException
* @throws NodeNotSubscribedException
* @throws NotToMeException
* @throws SizeMismatchException
*/ */
public function load(string $filename): void public function load(string $filename): File
{ {
Log::info(sprintf('%s:+ Processing TIC file [%s]',self::LOGKEY,$filename)); Log::info(sprintf('%s:+ Processing TIC file [%s]',self::LOGKEY,$filename));
$fs = Storage::disk(config('fido.local_disk'));
$rel_path_name = sprintf('%s/%s',config('fido.dir'),$filename);
if (str_contains($filename,'-')) { if (! $fs->exists($rel_path_name))
list($hex,$name) = explode('-',$filename); throw new FileNotFoundException(sprintf('File [%s] doesnt exist',$fs->path($rel_path_name)));
$hex = basename($hex);
} else { if ((! is_readable($fs->path($rel_path_name))) || ! ($f = $fs->readStream($rel_path_name)))
$hex = ''; throw new UnableToReadFile(sprintf('File [%s] is not readable',$fs->path($rel_path_name)));
/*
* Filenames are in the format X-Y-N.tic
* Where:
* - X is the nodes address that sent us the file
* - Y is the mtime of the TIC file from the sender
* - N is the sender's filename
*/
$aid = NULL;
$mtime = NULL;
$this->file->recv_tic = preg_replace('/\.[Tt][Ii][Cc]$/','',$filename);
$m = [];
if (preg_match(sprintf('/^%s\.[Tt][Ii][Cc]$/',Packet::regex),$filename,$m)) {
$aid = $m[1];
$mtime = $m[2];
$this->file->recv_tic = $m[3];
} }
if (! file_exists($filename)) $ldesc = '';
throw new FileNotFoundException(sprintf('File [%s] doesnt exist',$filename));
if (! is_readable($filename))
throw new UnableToWriteFile(sprintf('File [%s] is not readable',realpath($filename)));
$f = fopen($filename,'rb');
if (! $f) {
Log::error(sprintf('%s:! Unable to open file [%s] for writing',self::LOGKEY,$filename));
return;
}
while (! feof($f)) { while (! feof($f)) {
$line = chop(fgets($f)); $line = chop(fgets($f));
$matches = []; $m = [];
if (! $line) if (! $line)
continue; continue;
preg_match('/([a-zA-Z]+)\ (.*)/',$line,$matches); preg_match('/([a-zA-Z]+)\ ?(.*)?/',$line,$m);
if (in_array(strtolower($matches[1]),$this->_kludge)) { if (in_array(strtolower(Arr::get($m,1,'-')),$this->_kludge)) {
switch ($k=strtolower($matches[1])) { switch ($k=strtolower($m[1])) {
case 'area': case 'area':
$this->{$k} = Filearea::singleOrNew(['name'=>strtoupper($matches[2])]); try {
if ($fo=Filearea::where('name',strtoupper($m[2]))->firstOrFail())
$this->file->filearea_id = $fo->id;
} catch (ModelNotFoundException $e) {
// Rethrow this as No File Area
throw new NoFileAreaException($e->getMessage());
}
break; break;
case 'origin':
case 'from': case 'from':
case 'to': if (($ao=Address::findFTN($m[2])) && ((! $aid) || ($ao->zone->domain_id === Address::findOrFail(hexdec($aid))->zone->domain_id)))
$this->{$k} = Address::findFTN($matches[2]); $this->file->fftn_id = $ao->id;
else
throw new ModelNotFoundException(sprintf('FTN Address [%s] not found or sender mismatch',$m[2]));
break;
// The origin should be the first address in the path
case 'origin':
// Ignore
case 'areadesc':
case 'created':
break;
// This should be one of my addresses
case 'to':
$ftns = our_address()->pluck('ftn3d');
if (! ($ftns->contains($m[2])))
throw new NotToMeException(sprintf('FTN Address [%s] not found or not one of my addresses',$m[2]));
// @todo If $this->{$k} is null, we have discovered the system and it should be created
break; break;
case 'file': case 'file':
if (! Storage::disk('local')->exists($x=sprintf('%s/%s-%s',config('app.fido'),$hex,$matches[2]))) $this->file->name = $m[2];
throw new FileNotFoundException(sprintf('File not found? [%s]',$x));
$this->fo->name = $matches[2];
$this->fo->fullname = $x;
break;
case 'areadesc':
$areadesc = $matches[2];
break;
case 'created':
// ignored
break; break;
case 'pw': case 'pw':
$pw = $matches[2]; $pw = $m[2];
break; break;
case 'lfile': case 'lfile':
$this->fo->lname = $matches[2]; case 'fullname':
$this->file->lname = $m[2];
break; break;
case 'desc': case 'desc':
case 'magic': case 'magic':
case 'replaces': case 'replaces':
case 'size': case 'size':
$this->fo->{$k} = $matches[2]; $this->file->{$k} = $m[2];
break;
case 'fullname':
$this->fo->lfile = $matches[2];
break; break;
case 'date': case 'date':
$this->fo->datetime = Carbon::create($matches[2]); $this->file->datetime = Carbon::createFromTimestamp($m[2]);
break; break;
case 'ldesc': case 'ldesc':
$this->fo->{$k} .= $matches[2]; $ldesc .= ($ldesc ? "\r" : '').$m[2];
break; break;
case 'crc': case 'crc':
$this->fo->{$k} = hexdec($matches[2]); $this->file->{$k} = hexdec($m[2]);
break; break;
case 'path': case 'path':
$x = []; $this->file->set_path->push($m[2]);
preg_match(sprintf('#^[Pp]ath (%s)\ ?([0-9]+)\ ?(.*)$#',Address::ftn_regex),$line,$x);
$ao = Address::findFTN($x[1]);
if (! $ao) {
$this->fo->rogue_path->push($matches[2]);
} else {
$this->fo->set_path->push(['address'=>$ao,'datetime'=>Carbon::createFromTimestamp($x[8]),'extra'=>$x[9]]);
}
break; break;
case 'seenby': case 'seenby':
$ao = Address::findFTN($matches[2]); $this->file->set_seenby->push($m[2]);
if (! $ao) {
$this->fo->rogue_seenby->push($matches[2]);
} else {
$this->fo->set_seenby->push($ao->id);
}
break; break;
} }
} else { } else {
$this->fo->kludges->push($line); $this->file->kludges->push($line);
} }
} }
fclose($f); fclose($f);
$f = fopen($x=Storage::disk('local')->path($this->fo->fullname),'rb'); if ($ldesc)
$stat = fstat($f); $this->file->ldesc = $ldesc;
fclose($f);
// @todo Check that origin is the first address in the path
// @todo Make sure origin/from are in seenby
// @todo Make sure origin/from are in the path
/*
* Find our file and check the CRC
* If there is more than 1 file, select files that within 24hrs of the TIC file.
* If no files report file not found
* If there is more than 1 check each CRC to match the right one.
* If none match report, CRC error
*/
$found = FALSE;
$crcOK = FALSE;
foreach ($fs->files(config('fido.dir')) as $file) {
if (abs($x=$fs->lastModified($rel_path_name)-$fs->lastModified($file)) > 86400) {
Log::debug(sprintf('%s:/ Ignoring [%s] its mtime is outside of our scope [%d]',self::LOGKEY,$file,$x));
continue;
}
// Our file should have the same prefix as the TIC file
if (preg_match('#/'.($aid ? $aid.'-' : '').'.*'.$this->file->name.'$#',$file)) {
$found = TRUE;
if (sprintf('%08x',$this->file->crc) === ($y=$fs->checksum($file,['checksum_algo'=>'crc32b']))) {
$crcOK = TRUE;
break;
}
}
}
if (($found) && (! $crcOK))
throw new InvalidCRCException(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->file->crc,$fs->path($rel_path_name),$y));
elseif (! $found)
throw new FileNotFoundException(sprintf('File not found? [%s...%s] in [%s]',$aid,$this->file->name,$fs->path($rel_path_name)));
// @todo Add notifications back to the system if the replaces line doesnt match
if ($this->file->replaces && (! preg_match('/^'.$this->file->replaces.'$/',$this->file->name))) {
Log::alert(sprintf('%s:! Regex [%s] doesnt match file name [%s]',self::LOGKEY,$this->file->replaces,$this->file->name));
$this->file->replaces = NULL;
}
// @todo Add notification back to the system if no replaces line and the file already exists
// @todo Add notifictions back to the system
// Validate Size // Validate Size
if ($this->fo->size !== ($y=$stat['size'])) if ($this->file->size !== ($y=$fs->size($file)))
throw new \Exception(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->fo->size,$this->fo->fullname,$y)); throw new SizeMismatchException(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->file->size,$fs->path($rel_path_name),$y));
// Validate CRC
if (sprintf('%08x',$this->fo->crc) !== ($y=hash_file('crc32b',$x)))
throw new \Exception(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->fo->crc,$this->fo->fullname,$y));
// Validate Password // Validate Password
if ($pw !== ($y=$this->from->session('ticpass'))) if (strtoupper($pw) !== ($y=strtoupper($this->file->fftn->session('ticpass'))))
throw new \Exception(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->from->ftn,$y)); throw new InvalidPasswordException(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->file->fftn->ftn,$y));
// Validate Sender is linked (and permitted to send) // Validate Sender is linked
if ($this->from->fileareas->search(function($item) { return $item->id === $this->area->id; }) === FALSE) if ($this->file->fftn->fileareas->search(function($item) { return $item->id === $this->file->filearea_id; }) === FALSE)
throw new \Exception(sprintf('Node [%s] is not subscribed to [%s]',$this->from->ftn,$this->area->name)); throw new NodeNotSubscribedException(sprintf('Node [%s] is not subscribed to [%s]',$this->file->fftn->ftn,$this->file->filearea->name));
// If the filearea is to be autocreated, create it // Validate sender is permitted to write
if (! $this->area->exists) { // @todo Send a notification
$this->area->description = $areadesc; if (! $this->file->filearea->can_write($this->file->fftn->security))
$this->area->active = TRUE; throw new NoWriteSecurityException(sprintf('Node [%s] doesnt have enough security to write to [%s] (%d)',$this->file->fftn->ftn,$this->file->filearea->name,$this->file->fftn->security));
$this->area->show = FALSE;
$this->area->notes = 'Autocreated';
$this->area->domain_id = $this->from->zone->domain_id;
$this->area->save();
}
$this->fo->filearea_id = $this->area->id;
$this->fo->fftn_id = $this->origin->id;
// If the file create time is blank, we'll take the files // If the file create time is blank, we'll take the files
if (! $this->fo->datetime) if (! $this->file->datetime)
$this->fo->datetime = Carbon::createFromTimestamp($stat['ctime']); $this->file->datetime = Carbon::createFromTimestamp($fs->lastModified($file));
$this->fo->save(); $this->file->src_file = $file;
$this->file->recv_tic = $filename;
return $this->file;
}
public function save(): bool
{
return $this->file->save();
}
public function to(Address $ao): self
{
$this->to = $ao;
return $this;
} }
} }

View File

@ -7,6 +7,8 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\File\File as FileBase; use Symfony\Component\HttpFoundation\File\File as FileBase;
use App\Classes\FTN\Packet;
class File extends FileBase implements \Iterator class File extends FileBase implements \Iterator
{ {
private const LOGKEY = 'F--'; private const LOGKEY = 'F--';
@ -102,13 +104,13 @@ class File extends FileBase implements \Iterator
} }
/** /**
* Return the name of the file, without a node ID prefix * Return the name of the file, without a node ID, mtime prefix
* *
* @return string * @return string
*/ */
public function rawName(): string public function rawName(): string
{ {
return preg_replace('/^[0-9A-F]{4}-/','',$this->getFilename()); return preg_replace(sprintf('/^%s\.pkt$/i',Packet::regex),'\3\4',$this->getFilename());
} }
/** /**
@ -129,7 +131,7 @@ class File extends FileBase implements \Iterator
return preg_replace('/.pkt$/i','',Arr::get(stream_get_meta_data($f),'uri')); return preg_replace('/.pkt$/i','',Arr::get(stream_get_meta_data($f),'uri'));
} else { } else {
return $this->isPacket() ? preg_replace('/.pkt$/i','',$this->rawName()) : NULL; return $this->isPacket() ? $this->rawName() : NULL;
} }
} }
} }

View File

@ -48,7 +48,7 @@ abstract class Base
// 4 BITS of type // 4 BITS of type
protected const IS_FILE = (1<<0); protected const IS_FILE = (1<<0);
protected const IS_PKT = (1<<1); public const IS_PKT = (1<<1);
protected const IS_ARC = (1<<2); protected const IS_ARC = (1<<2);
protected const IS_REQ = (1<<3); protected const IS_REQ = (1<<3);
protected const IS_TIC = (1<<4); protected const IS_TIC = (1<<4);
@ -79,7 +79,7 @@ abstract class Base
return ($this->ftype&0xff) & $type; return ($this->ftype&0xff) & $type;
} }
protected function whatType(): int public function whatType(): int
{ {
static $ext = ['su','mo','tu','we','th','fr','sa','req']; static $ext = ['su','mo','tu','we','th','fr','sa','req'];

View File

@ -86,7 +86,7 @@ final class File extends Send
{ {
// If sending file is a File::class, then our file is s3 // If sending file is a File::class, then our file is s3
if ($this->nameas && $this->f instanceof FileModel) { if ($this->nameas && $this->f instanceof FileModel) {
$this->fd = Storage::readStream($this->f->full_storage_path); $this->fd = Storage::readStream($this->f->rel_name);
} else { } else {
$this->fd = fopen($this->full_name,'rb'); $this->fd = fopen($this->full_name,'rb');

View File

@ -2,14 +2,13 @@
namespace App\Classes\File; namespace App\Classes\File;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use App\Models\Address; use App\Models\Address;
final class Item extends Receive final class Item extends Receive
{ {
private const LOCATION = 'local';
/** @var Address The address that sent us this item */ /** @var Address The address that sent us this item */
private Address $ao; private Address $ao;
private string $recvas; private string $recvas;
@ -34,14 +33,14 @@ final class Item extends Receive
public function __get($key) { public function __get($key) {
switch ($key) { switch ($key) {
case 'exists': case 'exists':
return Storage::disk(self::LOCATION)->exists($this->rel_name); return Storage::disk(config('fido.local_disk'))->exists($this->rel_name);
case 'stor_name': case 'pref_name':
return sprintf('%04X-%s',$this->ao->id,$this->recvas); return sprintf('%04X-%d-%s',$this->ao->id,$this->recvmtime,$this->recvas);
case 'rel_name': case 'rel_name':
return sprintf('%s/%s',config('app.fido'),$this->stor_name); return sprintf('%s/%s',config('fido.dir'),$this->pref_name);
case 'full_name': case 'full_name':
return Storage::disk(self::LOCATION)->path($this->rel_name); return Storage::disk(config('fido.local_disk'))->path($this->rel_name);
case 'match_mtime': case 'match_mtime':
return $this->mtime === $this->recvmtime; return $this->mtime === $this->recvmtime;
@ -59,10 +58,10 @@ final class Item extends Receive
return sprintf('%s %lu %lu',$this->recvas,$this->recvsize,$this->recvmtime); return sprintf('%s %lu %lu',$this->recvas,$this->recvsize,$this->recvmtime);
case 'mtime': case 'mtime':
return Storage::disk(self::LOCATION)->lastModified($this->rel_name); return Storage::disk(config('fido.local_disk'))->lastModified($this->rel_name);
case 'size': case 'size':
return Storage::disk(self::LOCATION)->size($this->rel_name); return Storage::disk(config('fido.local_disk'))->size($this->rel_name);
default: default:
return parent::__get($key); return parent::__get($key);

View File

@ -5,14 +5,18 @@ namespace App\Classes\File;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Classes\Node; use App\Classes\Node;
use App\Classes\FTN\{Message,Packet}; use App\Classes\FTN\{Message,Packet};
final class Mail extends Send final class Mail extends Send
{ {
private const LOGKEY = 'IFM';
/** @var int Our internal position counter */ /** @var int Our internal position counter */
private int $readpos; private int $readpos;
private ?string $content;
/** /**
* @throws \Exception * @throws \Exception
@ -29,7 +33,7 @@ final class Mail extends Send
public function __get($key) { public function __get($key) {
switch ($key) { switch ($key) {
case 'dbids': case 'dbids':
return $this->f->messages->pluck('dbid'); return $this->f->messages->pluck('id');
case 'name': case 'name':
return sprintf('%08x',timew($this->youngest())); return sprintf('%08x',timew($this->youngest()));
@ -56,6 +60,8 @@ final class Mail extends Send
if ($successful) { if ($successful) {
$this->complete = TRUE; $this->complete = TRUE;
Log::debug(sprintf('%s:- Successful close for [%d] - updating [%d] records.',self::LOGKEY,$this->type,$this->dbids->count()),['dbids'=>$this->dbids,'authd'=>$node->aka_remote_authed->pluck('id')]);
// Update netmail table // Update netmail table
if (($this->type === Send::T_NETMAIL) if (($this->type === Send::T_NETMAIL)
&& ($x=$this->dbids)->count()) && ($x=$this->dbids)->count())
@ -79,6 +85,8 @@ final class Mail extends Send
'sent_at'=>Carbon::now(), 'sent_at'=>Carbon::now(),
'sent_pkt'=>$this->name, 'sent_pkt'=>$this->name,
]); ]);
$this->content = NULL;
} }
} }
@ -89,12 +97,13 @@ final class Mail extends Send
public function open(string $compress=''): bool public function open(string $compress=''): bool
{ {
$this->content = (string)$this->f;
return TRUE; return TRUE;
} }
public function read(int $length): string public function read(int $length): string
{ {
$result = substr((string)$this->f,$this->readpos,$length); $result = substr($this->content,$this->readpos,$length);
$this->readpos += strlen($result); $this->readpos += strlen($result);
return $result; return $result;
@ -106,7 +115,7 @@ final class Mail extends Send
return TRUE; return TRUE;
} }
public function youngest(): Carbon private function youngest(): Carbon
{ {
return $this->f->messages->pluck('date')->sort()->last(); return $this->f->messages->pluck('date')->sort()->last();
} }

View File

@ -5,15 +5,12 @@ namespace App\Classes\File;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileException;
use App\Classes\{File,Protocol}; use App\Classes\Protocol;
use App\Classes\FTN\{InvalidPacketException,Packet};
use App\Exceptions\FileGrewException; use App\Exceptions\FileGrewException;
use App\Jobs\{MessageProcess,TicProcess}; use App\Jobs\{PacketProcess,TicProcess};
use App\Models\Address; use App\Models\Address;
use App\Notifications\Netmails\PacketPasswordInvalid;
/** /**
* Object representing the files we are receiving * Object representing the files we are receiving
@ -37,6 +34,7 @@ class Receive extends Base
private ?string $comp; private ?string $comp;
/** @var string|null The compressed data received */ /** @var string|null The compressed data received */
private ?string $comp_data; private ?string $comp_data;
private $queue = FALSE;
public function __construct() public function __construct()
{ {
@ -63,7 +61,7 @@ class Receive extends Base
case 'nameas': case 'nameas':
case 'size': case 'size':
case 'name_size_time': case 'name_size_time':
case 'stor_name': case 'pref_name':
return $this->receiving->{$key}; return $this->receiving->{$key};
case 'pos': case 'pos':
@ -117,98 +115,25 @@ class Receive extends Base
fclose($this->f); fclose($this->f);
// Set our mtime // Set our mtime
Log::info(sprintf('%s:= Setting file [%s] to time [%s]',self::LOGKEY,$this->receiving->full_name,$this->receiving->recvmtime)); Log::debug(sprintf('%s:= Setting file [%s] to time [%s]',self::LOGKEY,$this->receiving->full_name,$this->receiving->recvmtime));
touch($this->receiving->full_name,$this->receiving->recvmtime); touch($this->receiving->full_name,$this->receiving->recvmtime);
$this->f = NULL; $this->f = NULL;
// If we received a packet, we'll dispatch a job to process it, if we got it all // If we received a packet, we'll dispatch a job to process it, if we got it all
if ($this->receiving->complete) if ($this->receiving->complete)
switch ($x=$this->receiving->whatType()) { switch ($this->receiving->whatType()) {
case self::IS_ARC: case self::IS_ARC:
case self::IS_PKT: case self::IS_PKT:
Log::info(sprintf('%s:- Processing mail %s [%s]',self::LOGKEY,$x === self::IS_PKT ? 'PACKET' : 'ARCHIVE',$this->receiving->nameas));
try { try {
$f = new File($this->receiving->full_name); // If packet is greater than a size, lets queue it
$processed = FALSE; if ($this->queue || ($this->receiving->size > config('fido.queue_size',0))) {
Log::info(sprintf('%s:- Packet [%s] will be sent to the queue for processing because its [%d] size, or queue forced',self::LOGKEY,$this->receiving->full_name,$this->receiving->size));
foreach ($f as $packet) { PacketProcess::dispatch($this->receiving->rel_name,$this->ao->zone->domain,FALSE,$rcvd_time);
$po = Packet::process($packet,Arr::get(stream_get_meta_data($packet),'uri'),$f->itemSize(),$this->ao->system); } else
PacketProcess::dispatchSync($this->receiving->rel_name,$this->ao->zone->domain,TRUE,$rcvd_time);
// Check the messages are from the uplink
if ($this->ao->system->addresses->search(function($item) use ($po) { return $item->id === $po->fftn_o->id; }) === FALSE) {
Log::error(sprintf('%s:! Packet [%s] is not from this link? [%d]',self::LOGKEY,$po->fftn_o->ftn,$this->ao->system_id));
break;
}
// Check the packet password
if ($this->ao->session('pktpass') !== $po->password) {
Log::error(sprintf('%s:! Packet from [%s] with password [%s] is invalid.',self::LOGKEY,$this->ao->ftn,$po->password));
Notification::route('netmail',$this->ao)->notify(new PacketPasswordInvalid($po->password,$this->receiving->nameas));
break;
}
Log::info(sprintf('%s:- Packet has [%d] messages',self::LOGKEY,$po->count()));
// Queue messages if there are too many in the packet.
if ($queue = ($po->count() > config('app.queue_msgs')))
Log::info(sprintf('%s:- Messages will be sent to the queue for processing',self::LOGKEY));
$count = 0;
foreach ($po as $msg) {
Log::info(sprintf('%s:- Mail from [%s] to [%s]',self::LOGKEY,$msg->fftn,$msg->tftn));
// @todo Quick check that the packet should be processed by us.
// @todo validate that the packet's zone is in the domain.
/*
* // @todo generate exception when echomail for an area that doesnt exist
* // @todo generate exception when echomail for an area sender cannot post to
* // @todo generate exception when echomail for an area sender not subscribed to
* // @todo generate exception when echomail comes from a system not defined here
* // @todo generate exception when echomail comes from a system doesnt exist
*
* // @todo generate exception when netmail to system that doesnt exist (node/point)
* // @todo generate exception when netmail from system that doesnt exist (node/point)
* // @todo generate warning when netmail comes from a system not defined here
*
* // @todo generate exception when packet has wrong password
*/
try {
// Dispatch job.
if ($queue)
MessageProcess::dispatch($msg,$f->pktName(),$this->ao,$po->fftn_o,$rcvd_time);
else
MessageProcess::dispatchSync($msg,$f->pktName(),$this->ao,$po->fftn_o,$rcvd_time);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error(sprintf('%s:! Got error dispatching message [%s] (%d:%s-%s).',self::LOGKEY,$msg->msgid,$e->getLine(),$e->getFile(),$e->getMessage())); Log::error(sprintf('%s:! Got error dispatching packet [%s] (%d:%s-%s).',self::LOGKEY,$this->receiving->rel_name,$e->getLine(),$e->getFile(),$e->getMessage()));
}
$count++;
}
if ($count === $po->count())
$processed = TRUE;
}
if (! $processed) {
Log::alert(sprintf('%s:- Not deleting packet [%s], it doesnt seem to be processed?',self::LOGKEY,$this->receiving->nameas));
// If we want to keep the packet, we could do that logic here
} elseif (! config('app.packet_keep')) {
Log::debug(sprintf('%s:- Deleting processed packet [%s]',self::LOGKEY,$this->receiving->full_name));
unlink($this->receiving->full_name);
}
} catch (InvalidPacketException $e) {
Log::error(sprintf('%s:- Not deleting packet [%s], as it generated an InvalidPacketException',self::LOGKEY,$this->receiving->nameas),['e'=>$e->getMessage()]);
} catch (\Exception $e) {
Log::error(sprintf('%s:- Not deleting packet [%s], as it generated an uncaught exception',self::LOGKEY,$this->receiving->nameas),['e'=>$e->getMessage()]);
} }
break; break;
@ -217,7 +142,7 @@ class Receive extends Base
Log::info(sprintf('%s:- Processing TIC file [%s]',self::LOGKEY,$this->receiving->nameas)); Log::info(sprintf('%s:- Processing TIC file [%s]',self::LOGKEY,$this->receiving->nameas));
// Queue the tic to be processed later, in case the referenced file hasnt been received yet // Queue the tic to be processed later, in case the referenced file hasnt been received yet
TicProcess::dispatch($this->receiving->rel_name); TicProcess::dispatch($this->receiving->pref_name)->delay(60);
break; break;
@ -232,11 +157,12 @@ class Receive extends Base
/** /**
* Add a new file to receive * Add a new file to receive
* *
* @param array $file * @param array $file The name of the receiving file
* @param Address $ao * @param Address $ao Sender sending a file to us
* @param bool $queue Force sending received items to the queue
* @throws \Exception * @throws \Exception
*/ */
public function new(array $file,Address $ao): void public function new(array $file,Address $ao,$queue=FALSE): void
{ {
Log::debug(sprintf('%s:+ Receiving new file [%s]',self::LOGKEY,join('|',$file))); Log::debug(sprintf('%s:+ Receiving new file [%s]',self::LOGKEY,join('|',$file)));
@ -244,6 +170,7 @@ class Receive extends Base
throw new \Exception('Can only have 1 file receiving at a time'); throw new \Exception('Can only have 1 file receiving at a time');
$this->ao = $ao; $this->ao = $ao;
$this->queue = $queue;
$this->list->push(new Item($ao,Arr::get($file,'name'),(int)Arr::get($file,'mtime'),(int)Arr::get($file,'size'))); $this->list->push(new Item($ao,Arr::get($file,'name'),(int)Arr::get($file,'mtime'),(int)Arr::get($file,'size')));
$this->index = $this->list->count()-1; $this->index = $this->list->count()-1;

View File

@ -6,6 +6,7 @@ use Exception;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileEncountered; use League\Flysystem\UnreadableFileEncountered;
use App\Classes\File\Send\Dynamic;
use App\Classes\Node; use App\Classes\Node;
use App\Models\Address; use App\Models\Address;
@ -122,13 +123,38 @@ class Send extends Base
if ($successful) { if ($successful) {
$end = time()-$this->start; $end = time()-$this->start;
Log::debug(sprintf('%s: - Closing [%s], sent in [%d] with [%s] items',self::LOGKEY,$this->sending->nameas,$end,$this->sending->dbids->count())); Log::info(sprintf('%s:- Closing [%s], sent in [%d] with [%s] items',self::LOGKEY,$this->sending->nameas,$end,$this->sending->dbids->count()));
} }
$this->sending->close($successful,$node); $this->sending->close($successful,$node);
$this->index = NULL; $this->index = NULL;
} }
public function dynamic(Address $ao): bool
{
$file = FALSE;
// If the node is marked as hold - dont send any files.
if ($ao->system->hold) {
Log::info(sprintf('%s:- System [%d] is marked as hold - not checking for files.',self::LOGKEY,$ao->system_id));
return FALSE;
}
// Files
if (($x=$ao->dynamicWaiting())->count()) {
Log::debug(sprintf('%s:- [%d] Dynamic Files(s) added for sending to [%s]',self::LOGKEY,$x->count(),$ao->ftn));
// Add Files
foreach ($x as $do)
$this->list->push(new Dynamic($do,$ao,self::T_FILE));
$file = TRUE;
}
return $file;
}
/* /*
private function compress(string $comp_mode): void private function compress(string $comp_mode): void
{ {
@ -202,7 +228,7 @@ class Send extends Base
if ((($this->index=$this->list->search(function($item) { return $item->complete === FALSE; })) !== FALSE) if ((($this->index=$this->list->search(function($item) { return $item->complete === FALSE; })) !== FALSE)
&& $this->sending->open()) && $this->sending->open())
{ {
Log::debug(sprintf('%s:- Sending item [%d] (%s)',self::LOGKEY,$this->index,$this->sending->nameas)); Log::info(sprintf('%s:- Sending item [%d] (%s)',self::LOGKEY,$this->index,$this->sending->nameas));
$this->pos = 0; $this->pos = 0;
$this->start = time(); $this->start = time();
@ -223,11 +249,10 @@ class Send extends Base
* Add our mail to the send queue * Add our mail to the send queue
* *
* @param Address $ao * @param Address $ao
* @param bool $update
* @return bool * @return bool
* @throws Exception * @throws Exception
*/ */
public function mail(Address $ao,bool $update=TRUE): bool public function mail(Address $ao): bool
{ {
$mail = FALSE; $mail = FALSE;
@ -239,7 +264,7 @@ class Send extends Base
} }
// Netmail // Netmail
if ($x=$ao->getNetmail($update)) { if ($x=$ao->getNetmail()) {
Log::debug(sprintf('%s:- Netmail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn)); Log::debug(sprintf('%s:- Netmail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn));
$this->list->push(new Mail($x,self::T_NETMAIL)); $this->list->push(new Mail($x,self::T_NETMAIL));
@ -247,7 +272,7 @@ class Send extends Base
} }
// Echomail // Echomail
if ($x=$ao->getEchomail($update)) { if ($x=$ao->getEchomail()) {
Log::debug(sprintf('%s:- Echomail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn)); Log::debug(sprintf('%s:- Echomail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn));
$this->list->push(new Mail($x,self::T_ECHOMAIL)); $this->list->push(new Mail($x,self::T_ECHOMAIL));

View File

@ -0,0 +1,124 @@
<?php
namespace App\Classes\File\Send;
use Carbon\Carbon;
use App\Classes\File\Send;
use App\Classes\Node;
use App\Models\Address;
use App\Models\Dynamic as Model;
use App\Classes\Dynamic as Item;
/**
* Dynamic files that are sent to systems during a mailer session
*/
final class Dynamic extends Send
{
private const LOGKEY = 'FSD';
/** @var int Our internal position counter */
private int $readpos = 0;
private string $buffer;
private Item $item;
private Carbon $sent;
/**
* @throws \Exception
*/
public function __construct(private Model $do,Address $ao,int $type)
{
parent::__construct();
$this->ftype = ((($type&0xff)<<8)|self::IS_FILE);
$this->item = new $this->do->model($ao,$this->do->arguments);
$this->sent = Carbon::now();
}
public function __get($key) {
switch ($key) {
case 'dbids':
return collect([$this->do->id]);
case 'nameas':
return $this->item->getName();
case 'mtime':
return $this->sent->timestamp;
case 'size':
return strlen($this->buffer);
default:
return NULL;
}
}
public function close(bool $successful,Node $node): void
{
if ($successful) {
$this->complete = TRUE;
$next_at = $this->do->next_at
->startOfDay()
->addHours($this->do->start_time->hour)
->addMinutes($this->do->start_time->minute);
switch ($this->do->frequency) {
case 'ONCE':
$this->do->active = FALSE;
break;
case 'DAILY':
$this->do->next_at = $next_at
->addDay();
break;
case 'WEEKLY':
$this->do->next_at = $next_at
->addWeek();
break;
case 'MONTHLY':
$this->do->next_at = $next_at
->addMonth();
break;
default:
throw new \Exception(sprintf('%s:! Unknown frequency [%s] for [%d]',self::LOGKEY,$this->do->frequency,$this->do->id));
}
$this->do->save();
}
}
public function feof(): bool
{
return ($this->readpos === $this->size);
}
public function open(string $compress=''): bool
{
$this->buffer = (string)$this->item;
return TRUE;
}
public function read(int $length): string
{
$result = substr($this->buffer,$this->readpos,$length);
$this->readpos += strlen($result);
return $result;
}
public function seek(int $pos): bool
{
$this->readpos = ($pos < $this->size) ? $pos : $this->size;
return TRUE;
}
}

View File

@ -26,7 +26,7 @@ final class Tic extends Send
$this->ftype = ((($type&0xff)<<8)|self::IS_TIC); $this->ftype = ((($type&0xff)<<8)|self::IS_TIC);
$this->readpos = 0; $this->readpos = 0;
$this->tic = FTNTic::generate($ao,$file); $this->tic = (string)(new FTNTic($file))->to($ao);
} }
public function __get($key) { public function __get($key) {

View File

@ -92,7 +92,7 @@ class Node
// Return how long our session has been connected // Return how long our session has been connected
case 'session_time': case 'session_time':
return Carbon::now()->diffInSeconds($this->start_time); return sprintf("%d",$this->start_time->diffInSeconds(Carbon::now()));
case 'system': case 'system':
case 'sysop': case 'sysop':

View File

@ -186,6 +186,8 @@ class Page
$this->text .= $text; $this->text .= $text;
$this->text_right = $right; $this->text_right = $right;
return $this;
} }
/** /**

View File

@ -9,7 +9,9 @@ use App\Classes\File\{Receive,Send};
use App\Classes\Protocol\EMSI; use App\Classes\Protocol\EMSI;
use App\Classes\Sock\SocketClient; use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException; use App\Classes\Sock\SocketException;
use App\Models\{Address,Setup,System,SystemLog}; use App\Models\{Address,Mailer,Setup,System,SystemLog};
// @todo after receiving a mail packet/file, dont acknowledge it until we can validate that we can read it properly.
abstract class Protocol abstract class Protocol
{ {
@ -26,14 +28,6 @@ abstract class Protocol
protected const RCDO = -3; protected const RCDO = -3;
protected const ERROR = -5; protected const ERROR = -5;
// Our sessions Types
public const SESSION_AUTO = 0;
/** @deprecate Use mailers:class */
public const SESSION_EMSI = 1;
/** @deprecate Use mailers:class */
public const SESSION_BINKP = 2;
public const SESSION_ZMODEM = 3;
protected const MAX_PATH = 1024; protected const MAX_PATH = 1024;
/* O_ options - [First 9 bits are protocol P_* (Interfaces/ZModem)] */ /* O_ options - [First 9 bits are protocol P_* (Interfaces/ZModem)] */
@ -132,9 +126,11 @@ abstract class Protocol
private array $comms; private array $comms;
protected bool $force_queue = FALSE;
abstract protected function protocol_init(): int; abstract protected function protocol_init(): int;
abstract protected function protocol_session(): int; abstract protected function protocol_session(bool $force_queue=FALSE): int;
public function __construct(Setup $o=NULL) public function __construct(Setup $o=NULL)
{ {
@ -174,6 +170,10 @@ abstract class Protocol
$this->comms[$key] = $value; $this->comms[$key] = $value;
break; break;
case 'client':
$this->{$key} = $value;
break;
default: default:
throw new \Exception('Unknown key: '.$key); throw new \Exception('Unknown key: '.$key);
} }
@ -309,14 +309,14 @@ abstract class Protocol
$addresses = collect(); $addresses = collect();
foreach (($this->originate ? $this->node->aka_remote_authed : $this->node->aka_remote) as $ao) foreach (($this->originate ? $this->node->aka_remote_authed : $this->node->aka_remote) as $ao)
$addresses = $addresses->merge($this->setup->system->match($ao->zone,Address::NODE_ZC|Address::NODE_RC|Address::NODE_NC|Address::NODE_HC|Address::NODE_ACTIVE|Address::NODE_PVT|Address::NODE_POINT)); $addresses = $addresses->merge(our_address($ao->zone->domain));
$addresses = $addresses->unique(); $addresses = $addresses->unique();
Log::debug(sprintf('%s:- Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(','))); Log::debug(sprintf('%s:- Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
} else { } else {
$addresses = $this->setup->system->addresses; $addresses = $this->setup->system->akas;
Log::debug(sprintf('%s:- Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(','))); Log::debug(sprintf('%s:- Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
} }
@ -327,13 +327,13 @@ abstract class Protocol
/** /**
* Initialise our Session * Initialise our Session
* *
* @param int $type * @param Mailer $mo
* @param SocketClient $client * @param SocketClient $client
* @param Address|null $o * @param Address|null $o
* @return int * @return int
* @throws \Exception * @throws \Exception
*/ */
public function session(int $type,SocketClient $client,Address $o=NULL): int public function session(Mailer $mo,SocketClient $client,Address $o=NULL): int
{ {
if ($o->exists) if ($o->exists)
Log::withContext(['ftn'=>$o->ftn]); Log::withContext(['ftn'=>$o->ftn]);
@ -365,12 +365,11 @@ abstract class Protocol
// We are an IP node // We are an IP node
$this->optionSet(self::O_TCP); $this->optionSet(self::O_TCP);
$this->setClient($client); $this->client = $client;
switch ($type) { switch ($mo->name) {
/** @noinspection PhpMissingBreakStatementInspection */ case 'EMSI':
case self::SESSION_AUTO: Log::debug(sprintf('%s:- Starting EMSI',self::LOGKEY));
Log::debug(sprintf('%s:- Trying EMSI',self::LOGKEY));
$rc = $this->protocol_init(); $rc = $this->protocol_init();
if ($rc < 0) { if ($rc < 0) {
@ -379,27 +378,19 @@ abstract class Protocol
return self::S_FAILURE; return self::S_FAILURE;
} }
case self::SESSION_EMSI: $rc = $this->protocol_session($this->originate);
Log::debug(sprintf('%s:- Starting EMSI',self::LOGKEY));
$rc = $this->protocol_session();
break; break;
case self::SESSION_BINKP: case 'BINKP':
Log::debug(sprintf('%s:- Starting BINKP',self::LOGKEY)); Log::debug(sprintf('%s:- Starting BINKP',self::LOGKEY));
$rc = $this->protocol_session();
$rc = $this->protocol_session($this->originate);
break; break;
case self::SESSION_ZMODEM:
Log::debug(sprintf('%s:- Starting ZMODEM',self::LOGKEY));
$this->client->speed = self::TCP_SPEED;
$this->originate = FALSE;
return $this->protocol_session();
default: default:
Log::error(sprintf('%s:! Unsupported session type [%d]',self::LOGKEY,$type)); Log::error(sprintf('%s:! Unsupported session type [%d]',self::LOGKEY,$mo->id));
return self::S_FAILURE; return self::S_FAILURE;
} }
@ -429,15 +420,16 @@ abstract class Protocol
)); ));
// Add unknown FTNs to the DB // Add unknown FTNs to the DB
if ($this->node->aka_remote_authed->count()) { $so = ($this->node->aka_remote_authed->count())
$so = $this->node->aka_remote_authed->first()->system; ? $this->node->aka_remote_authed->first()->system
} else { : System::createUnknownSystem();
$so = System::where('name','Discovered System')->single();
}
if ($so && $so->exists) { if ($so && $so->exists) {
foreach ($this->node->aka_other as $aka) { foreach ($this->node->aka_other as $aka)
Address::findFTN($aka,TRUE,$so); if (! Address::findFTN($aka)) {
$oo = Address::createFTN($aka,$so);
$oo->validated = TRUE;
$oo->save();
} }
// Log session in DB // Log session in DB
@ -446,12 +438,18 @@ abstract class Protocol
$slo->items_sent_size = $this->send->total_sent_bytes; $slo->items_sent_size = $this->send->total_sent_bytes;
$slo->items_recv = $this->recv->total_recv; $slo->items_recv = $this->recv->total_recv;
$slo->items_recv_size = $this->recv->total_recv_bytes; $slo->items_recv_size = $this->recv->total_recv_bytes;
$slo->sessiontype = $type; $slo->mailer_id = $mo->id;
$slo->sessiontime = $this->node->session_time; $slo->sessiontime = $this->node->session_time;
$slo->result = ($rc & self::S_MASK); $slo->result = ($rc & self::S_MASK);
$slo->originate = $this->originate; $slo->originate = $this->originate;
$so->logs()->save($slo); $so->logs()->save($slo);
// If we are autohold, then remove that
if ($so->autohold) {
$so->autohold = FALSE;
$so->save();
}
} }
// @todo Optional after session execution event // @todo Optional after session execution event
@ -495,15 +493,4 @@ abstract class Protocol
{ {
$this->session |= $key; $this->session |= $key;
} }
/**
* Set our client that we are communicating with
*
* @param SocketClient $client
* @deprecated use __get()/__set()
*/
protected function setClient(SocketClient $client): void
{
$this->client = $client;
}
} }

View File

@ -9,11 +9,12 @@ use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileEncountered; use League\Flysystem\UnreadableFileEncountered;
use App\Classes\Crypt; use App\Classes\Crypt;
use App\Classes\Node;
use App\Classes\Protocol as BaseProtocol; use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient; use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException; use App\Classes\Sock\SocketException;
use App\Exceptions\FileGrewException; use App\Exceptions\{FileGrewException,InvalidFTNException};
use App\Models\Address; use App\Models\{Address,Mailer};
final class Binkp extends BaseProtocol final class Binkp extends BaseProtocol
{ {
@ -147,7 +148,6 @@ final class Binkp extends BaseProtocol
* @param SocketClient $client * @param SocketClient $client
* @return int|null * @return int|null
* @throws SocketException * @throws SocketException
* @throws \Exception
*/ */
public function onConnect(SocketClient $client): ?int public function onConnect(SocketClient $client): ?int
{ {
@ -155,8 +155,7 @@ final class Binkp extends BaseProtocol
if (! parent::onConnect($client)) { if (! parent::onConnect($client)) {
Log::withContext(['pid'=>getmypid()]); Log::withContext(['pid'=>getmypid()]);
// @todo If we can use SESSION_EMSI set an object class value that in BINKP of SESSION_BINKP, and move this method to the parent class $this->session(Mailer::where('name','BINKP')->singleOrFail(),$client,(new Address));
$this->session(self::SESSION_BINKP,$client,(new Address));
$this->client->close(); $this->client->close();
exit(0); exit(0);
} }
@ -431,17 +430,23 @@ final class Binkp extends BaseProtocol
$rc = TRUE; $rc = TRUE;
} else { } else {
$data = substr($this->rx_buf,1); // http://ftsc.org/docs/fts-1026.001 - frames may be NULL terminated
$data = rtrim(substr($this->rx_buf,1),"\x00");
switch ($msg) { switch ($msg) {
case self::BPM_ADR: case self::BPM_ADR:
Log::debug(sprintf('%s:- ADR:Address [%s]',self::LOGKEY,$data)); Log::debug(sprintf('%s:- ADR:Address [%s]',self::LOGKEY,$data));
$rc = $this->M_adr($data); // @note It seems taurus may pad data with nulls at the end (esp BPM_ADR), so we should trim that.
$rc = $this->M_adr(trim($data));
break; break;
case self::BPM_EOB: case self::BPM_EOB:
Log::debug(sprintf('%s:- EOB:We got an EOB message with [%d] chars in the buffer',self::LOGKEY,strlen($data))); Log::debug(sprintf('%s:- EOB:We got an EOB message with [%d] chars in the buffer',self::LOGKEY,strlen($data)));
$rc = $this->M_eob($data);
if (strlen($data))
Log::critical(sprintf('%s:! EOB but we have data?',self::LOGKEY),['data'=>$data]);
$rc = $this->M_eob();
break; break;
case self::BPM_NUL: case self::BPM_NUL:
@ -451,7 +456,7 @@ final class Binkp extends BaseProtocol
case self::BPM_PWD: case self::BPM_PWD:
Log::debug(sprintf('%s:- PWD:We got a password [%s]',self::LOGKEY,$data)); Log::debug(sprintf('%s:- PWD:We got a password [%s]',self::LOGKEY,$data));
$rc = $this->M_pwd($data); $rc = $this->M_pwd(ltrim($data));
break; break;
case self::BPM_ERR: case self::BPM_ERR:
@ -476,7 +481,7 @@ final class Binkp extends BaseProtocol
case self::BPM_OK: case self::BPM_OK:
Log::debug(sprintf('%s:- OK:Got an OK [%s]',self::LOGKEY,$data)); Log::debug(sprintf('%s:- OK:Got an OK [%s]',self::LOGKEY,$data));
$rc = $this->M_ok($data); $rc = $this->M_ok(ltrim($data));
break; break;
case self::BPM_CHAT: case self::BPM_CHAT:
@ -680,26 +685,44 @@ final class Binkp extends BaseProtocol
*/ */
private function M_adr(string $buf): bool private function M_adr(string $buf): bool
{ {
$buf = $this->skip_blanks($buf);
$rc = 0; $rc = 0;
while ($rem_aka=$this->strsep($buf,' ')) { while ($rem_aka=$this->strsep($buf,' ')) {
try { try {
if (! ($o=Address::findFTN($rem_aka,FALSE,NULL,TRUE))) { if (! ($o=Address::findFTN($rem_aka,TRUE))) {
// @todo when we have multiple inactive records, this returns more than 1, so pluck the active record if there is one // @todo when we have multiple inactive records, this returns more than 1, so pluck the active record if there is one
Log::alert(sprintf('%s:? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka)); Log::alert(sprintf('%s:? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));
$this->node->ftn_other = $rem_aka; $this->node->ftn_other = $rem_aka;
continue; continue;
// If we only present limited AKAs dont validate password against akas outside of the domains we present
} elseif (is_null(our_address($o))) {
Log::alert(sprintf('%s:/ AKA domain [%s] is not in our domain(s) [%s] - ignoring',self::LOGKEY,$o->zone->domain->name,our_address()->pluck('zone.domain.name')->unique()->join(',')));
$this->node->ftn_other = $rem_aka;
continue;
} elseif (! $o->active) { } elseif (! $o->active) {
Log::alert(sprintf('%s:/ AKA is not active [%s], ignoring',self::LOGKEY,$rem_aka)); Log::alert(sprintf('%s:/ AKA is not active [%s] - ignoring',self::LOGKEY,$rem_aka));
$this->node->ftn_other = $rem_aka;
continue; continue;
} else { } else {
Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka)); Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka));
// We'll update this address status
$o->validated = TRUE;
$o->role &= ~(Address::NODE_HOLD|Address::NODE_DOWN);
$o->save();
} }
} catch (InvalidFTNException $e) {
Log::error(sprintf('%s:! AKA is INVALID [%s] (%s) - ignoring',self::LOGKEY,$rem_aka,$e->getMessage()));
continue;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error(sprintf('%s:! AKA is INVALID [%s] (%d:%s-%s)',self::LOGKEY,$rem_aka,$e->getLine(),$e->getFile(),$e->getMessage())); Log::error(sprintf('%s:! AKA is INVALID [%s] (%d:%s-%s)',self::LOGKEY,$rem_aka,$e->getLine(),$e->getFile(),$e->getMessage()));
@ -744,18 +767,14 @@ final class Binkp extends BaseProtocol
return 0; return 0;
} }
// Add our mail to the queue if we have authenticated /**
if ($this->node->aka_authed) * http://ftsc.org/docs/fts-1026.001
foreach ($this->node->aka_remote_authed as $ao) { * M_NUL "TRF netmail_bytes arcmail_bytes"
if (! $ao->validated) { * traffic prognosis (in bytes) for the netmail
Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn)); * (netmail_bytes) and arcmail + files (arcmail_bytes),
continue; * both are decimal ASCII strings
} */
// @todo This is affectively redundant, because we are not determining our mail until later
$this->send->mail($ao);
$this->send->files($ao);
}
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size)); $this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));
if ($this->md_challenge) { if ($this->md_challenge) {
@ -805,7 +824,7 @@ final class Binkp extends BaseProtocol
* *
* @throws \Exception * @throws \Exception
*/ */
private function M_eob(string $buf): bool private function M_eob(): bool
{ {
if ($this->recv->fd) { if ($this->recv->fd) {
Log::info(sprintf('%s:= Closing receiving file.',self::LOGKEY)); Log::info(sprintf('%s:= Closing receiving file.',self::LOGKEY));
@ -817,21 +836,8 @@ final class Binkp extends BaseProtocol
$this->sessionClear(self::SE_DELAYEOB); $this->sessionClear(self::SE_DELAYEOB);
if (! $this->send->togo_count && $this->sessionGet(self::SE_NOFILES) && $this->capGet(self::F_MULTIBATCH,self::O_YES)) { if (! $this->send->togo_count && $this->sessionGet(self::SE_NOFILES) && $this->capGet(self::F_MULTIBATCH,self::O_YES)) {
// Add our mail to the queue if we have authenticated $this->getFiles($this->node);
if ($this->node->aka_authed)
foreach ($this->node->aka_remote_authed as $ao) {
Log::debug(sprintf('%s:- Checking for any new mail and files to [%s]',self::LOGKEY,$ao->ftn));
if (! $ao->validated) {
Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn));
continue;
}
$this->send->mail($ao);
$this->send->files($ao);
}
Log::debug(sprintf('%s:- We have [%d] items to send to [%s]',self::LOGKEY,$this->send->togo_count,$ao->ftn));
if ($this->send->togo_count) if ($this->send->togo_count)
$this->sessionClear(self::SE_NOFILES|self::SE_SENTEOB); $this->sessionClear(self::SE_NOFILES|self::SE_SENTEOB);
} }
@ -892,7 +898,7 @@ final class Binkp extends BaseProtocol
return TRUE; return TRUE;
} }
$this->recv->new($file['file'],$this->node->address); $this->recv->new($file['file'],$this->node->address,$this->force_queue);
try { try {
switch ($this->recv->open($file['offs']<0,$file['flags'])) { switch ($this->recv->open($file['offs']<0,$file['flags'])) {
@ -1012,6 +1018,9 @@ final class Binkp extends BaseProtocol
$this->send->close(TRUE,$this->node); $this->send->close(TRUE,$this->node);
} }
} else {
Log::error(sprintf('%s:! M_got[skip] not for our file? [%s]',self::LOGKEY,$buf));
} }
} else { } else {
@ -1029,16 +1038,16 @@ final class Binkp extends BaseProtocol
Log::info(sprintf('%s:+ M_NUL [%s]',self::LOGKEY,$buf)); Log::info(sprintf('%s:+ M_NUL [%s]',self::LOGKEY,$buf));
if (! strncmp($buf,'SYS ',4)) { if (! strncmp($buf,'SYS ',4)) {
$this->node->system = $this->skip_blanks(substr($buf,4)); $this->node->system = ltrim(substr($buf,4));
} elseif (! strncmp($buf, 'ZYZ ',4)) { } elseif (! strncmp($buf, 'ZYZ ',4)) {
$this->node->sysop = $this->skip_blanks(substr($buf,4)); $this->node->sysop = ltrim(substr($buf,4));
} elseif (! strncmp($buf,'LOC ',4)) { } elseif (! strncmp($buf,'LOC ',4)) {
$this->node->location = $this->skip_blanks(substr($buf,4)); $this->node->location = ltrim(substr($buf,4));
} elseif (! strncmp($buf,'NDL ',4)) { } elseif (! strncmp($buf,'NDL ',4)) {
$data = $this->skip_blanks(substr($buf,4)); $data = ltrim(substr($buf,4));
$comma = strpos($data,','); $comma = strpos($data,',');
$spd = substr($data,0,$comma); $spd = substr($data,0,$comma);
@ -1049,7 +1058,7 @@ final class Binkp extends BaseProtocol
$this->client->speed = $spd; $this->client->speed = $spd;
} else { } else {
$comma = $this->skip_blanks(substr($buf,4)); $comma = ltrim(substr($buf,4));
$c = 0; $c = 0;
while (($x=substr($comma,$c,1)) && is_numeric($x)) while (($x=substr($comma,$c,1)) && is_numeric($x))
$c++; $c++;
@ -1071,19 +1080,25 @@ final class Binkp extends BaseProtocol
} }
} elseif (! strncmp($buf,'TIME ',5)) { } elseif (! strncmp($buf,'TIME ',5)) {
$this->node->node_time = $this->skip_blanks(substr($buf,5)); $this->node->node_time = ltrim(substr($buf,5));
} elseif (! strncmp($buf,'VER ',4)) { } elseif (! strncmp($buf,'VER ',4)) {
$data = $this->skip_blanks(substr($buf,4)); $data = ltrim(substr($buf,4));
$matches = []; $matches = [];
preg_match('#^(.+)\s+binkp/([0-9]+)\.([0-9]+)$#',$data,$matches); preg_match('#^(.+)\s+\(?binkp/([0-9]+)\.([0-9]+)\)?$#',$data,$matches);
if (count($matches) === 4) {
$this->node->software = $matches[1]; $this->node->software = $matches[1];
$this->node->ver_major = $matches[2]; $this->node->ver_major = $matches[2];
$this->node->ver_minor = $matches[3]; $this->node->ver_minor = $matches[3];
} else {
$this->node->software = 'Unknown';
$this->node->ver_major = 0;
$this->node->ver_minor = 0;
}
} elseif (! strncmp($buf,'TRF ',4)) { } elseif (! strncmp($buf,'TRF ',4)) {
$data = $this->skip_blanks(substr($buf,4)); $data = ltrim(substr($buf,4));
$matches = []; $matches = [];
preg_match('/^([0-9]+)\s+([0-9]+)$/',$data,$matches); preg_match('/^([0-9]+)\s+([0-9]+)$/',$data,$matches);
@ -1097,13 +1112,13 @@ final class Binkp extends BaseProtocol
$this->sessionSet(self::SE_DELAYEOB); $this->sessionSet(self::SE_DELAYEOB);
} elseif (! strncmp($buf,'PHN ',4)) { } elseif (! strncmp($buf,'PHN ',4)) {
$this->node->phone = $this->skip_blanks(substr($buf,4)); $this->node->phone = ltrim(substr($buf,4));
} elseif (! strncmp($buf,'OPM ',4)) { } elseif (! strncmp($buf,'OPM ',4)) {
$this->node->message = $this->skip_blanks(substr($buf,4)); $this->node->message = ltrim(substr($buf,4));
} elseif (! strncmp($buf,'OPT ',4)) { } elseif (! strncmp($buf,'OPT ',4)) {
$data = $this->skip_blanks(substr($buf,4)); $data = ltrim(substr($buf,4));
while ($data && ($p = $this->strsep($data,' '))) { while ($data && ($p = $this->strsep($data,' '))) {
if (! strcmp($p,'MB')) { if (! strcmp($p,'MB')) {
@ -1142,12 +1157,15 @@ final class Binkp extends BaseProtocol
$this->capSet(self::F_COMP,self::O_THEY|self::O_EXT); $this->capSet(self::F_COMP,self::O_THEY|self::O_EXT);
} elseif (! strncmp($p,'CRAM-MD5-',9) && $this->originate && $this->capGet(self::F_MD,self::O_WANT)) { } elseif (! strncmp($p,'CRAM-MD5-',9) && $this->originate && $this->capGet(self::F_MD,self::O_WANT)) {
if (($x=strlen(substr($p,9))) > 64 ) { if (strlen($hex=substr($p,9)) > 64 ) {
Log::error(sprintf('%s:! The challenge string is TOO LONG [%d] (%s)',self::LOGKEY,$x,$p)); Log::error(sprintf('%s:! The challenge string is TOO LONG [%d] (%s)',self::LOGKEY,strlen($hex),$p));
} elseif (strlen($hex)%2) {
Log::error(sprintf('%s:! The challenge string is an odd size [%d] (%s)',self::LOGKEY,strlen($hex),$hex));
} else { } else {
Log::info(sprintf('%s:- Remote wants MD5 auth',self::LOGKEY)); Log::info(sprintf('%s:- Remote wants MD5 auth with [%s]',self::LOGKEY,$hex));
$this->md_challenge = hex2bin(substr($p,9)); $this->md_challenge = hex2bin($hex);
if ($this->md_challenge) if ($this->md_challenge)
$this->capSet(self::F_MD,self::O_THEY); $this->capSet(self::F_MD,self::O_THEY);
@ -1184,8 +1202,6 @@ final class Binkp extends BaseProtocol
return FALSE; return FALSE;
} }
$buf = $this->skip_blanks($buf);
if ($this->optionGet(self::O_PWD) && $buf) { if ($this->optionGet(self::O_PWD) && $buf) {
while (($t=$this->strsep($buf," \t"))) while (($t=$this->strsep($buf," \t")))
if (strcmp($t,'non-secure') === 0) { if (strcmp($t,'non-secure') === 0) {
@ -1208,11 +1224,10 @@ final class Binkp extends BaseProtocol
} }
/** /**
* @throws \Exception * @todo It appears when we poll a node, we dont ask for passwords, but we still send echomail and files.
*/ */
private function M_pwd(string $buf): bool private function M_pwd(string $buf): bool
{ {
$buf = $this->skip_blanks($buf);
$have_CRAM = !strncasecmp($buf,'CRAM-MD5-',9); $have_CRAM = !strncasecmp($buf,'CRAM-MD5-',9);
$have_pwd = $this->optionGet(self::O_PWD); $have_pwd = $this->optionGet(self::O_PWD);
@ -1294,24 +1309,13 @@ final class Binkp extends BaseProtocol
if (strlen($opt)) if (strlen($opt))
$this->msgs(self::BPM_NUL,sprintf('OPT%s',$opt)); $this->msgs(self::BPM_NUL,sprintf('OPT%s',$opt));
// Add our mail to the queue if we have authenticated // @todo This is effectively redundant, because we are not getting files until later
if ($this->node->aka_authed) {
foreach ($this->node->aka_remote_authed as $ao) {
if (! $ao->validated) {
Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn));
continue;
}
$this->send->mail($ao);
$this->send->files($ao);
}
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size)); $this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));
if ($this->node->aka_authed) {
$this->msgs(self::BPM_OK,sprintf('%ssecure',$have_pwd ? '' : 'non-')); $this->msgs(self::BPM_OK,sprintf('%ssecure',$have_pwd ? '' : 'non-'));
} else { } else {
// @todo Send any direct netmail to this node, if that node is unknown to us
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));
$this->msgs(self::OK,'non-secure'); $this->msgs(self::OK,'non-secure');
} }
@ -1327,14 +1331,16 @@ final class Binkp extends BaseProtocol
/** /**
* Set up our BINKP session * Set up our BINKP session
* *
* @param bool $force_queue
* @return int * @return int
* @throws \Exception * @throws \Exception
*/ */
protected function protocol_session(): int protected function protocol_session(bool $force_queue=FALSE): int
{ {
if ($this->binkp_init() !== self::OK) if ($this->binkp_init() !== self::OK)
return self::S_FAILURE; return self::S_FAILURE;
$this->force_queue = $force_queue;
$this->binkp_hs(); $this->binkp_hs();
while (TRUE) { while (TRUE) {
@ -1344,9 +1350,12 @@ final class Binkp extends BaseProtocol
&& (! $this->sessionGet(self::SE_NOFILES)) && (! $this->sessionGet(self::SE_NOFILES))
&& (! $this->send->fd)) && (! $this->send->fd))
{ {
if (! $this->send->togo_count)
$this->getFiles($this->node);
// Open our next file to send // Open our next file to send
if ($this->send->togo_count && ! $this->send->fd) { if ($this->send->togo_count && ! $this->send->fd) {
Log::info(sprintf('%s:- Opening next file to send',self::LOGKEY)); Log::info(sprintf('%s:- Opening next file to send - we have [%d] left',self::LOGKEY,$this->send->togo_count));
$this->send->open(); $this->send->open();
} }
@ -1374,6 +1383,8 @@ final class Binkp extends BaseProtocol
// We dont have anything to send // We dont have anything to send
} else { } else {
Log::info(sprintf('%s:- Nothing left to send in this batch',self::LOGKEY)); Log::info(sprintf('%s:- Nothing left to send in this batch',self::LOGKEY));
// @todo We should look for more mail/files before thinking about sending an EOB
// IE: When we are set to only send X messages, but we have > X to send, get the next batch.
$this->sessionSet(self::SE_NOFILES); $this->sessionSet(self::SE_NOFILES);
} }
} }
@ -1478,13 +1489,50 @@ final class Binkp extends BaseProtocol
return $this->rc; return $this->rc;
} }
public function getFiles(Node $node): void
{
// Add our mail to the queue if we have authenticated
if ($node->aka_authed) {
Log::info(sprintf('%s:- We have authed these AKAs [%s]',self::LOGKEY,$node->aka_remote_authed->pluck('ftn')->join(',')));
foreach ($node->aka_remote_authed as $ao) {
Log::debug(sprintf('%s:- Checking for any new mail and files to [%s]',self::LOGKEY,$ao->ftn));
if (! $ao->validated) {
Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn));
continue;
}
$this->send->mail($ao);
$this->send->files($ao);
$this->send->dynamic($ao);
/*
* Add "dynamic files", eg: nodelist, nodelist segment, status reports.
* Dynamic files are built on the fly
* * query "dynamic" for items for the address
* * column 'method' identifies the method that will be called, with the $ao as the argument
* * a 'new Item' is added to the queue
* * when it its ready to be sent, the __tostring() is called that renders it
* * when sent, the dynamic table is updated with the sent_at
*/
}
Log::info(sprintf('%s:- We have [%d] items to send to [%s]',self::LOGKEY,$this->send->togo_count,$ao->system->name));
} else {
// @todo We should only send netmail if unauthenticated - netmail that is direct to this node (no routing)
Log::debug(sprintf('%s:- Not AUTHed so not looking for mail, but we know these akas [%s]',self::LOGKEY,$node->aka_remote->pluck('ftn')->join(',')));
}
}
/** /**
* Strip blanks at the beginning of a string * Strip blanks at the beginning of a string
* *
* @param string $str * @param string $str
* @return string * @return string
* @throws \Exception * @throws \Exception
* @todo Can this be replaced with ltrim? * @deprecated - use ltrim instead
*/ */
private function skip_blanks(string $str): string private function skip_blanks(string $str): string
{ {
@ -1524,6 +1572,7 @@ final class Binkp extends BaseProtocol
* @param string $str * @param string $str
* @return bool * @return bool
* @throws \Exception * @throws \Exception
* @deprecated No longer required since we are using ltrim
*/ */
private function isSpace(string $str):bool private function isSpace(string $str):bool
{ {

View File

@ -7,11 +7,11 @@ use Illuminate\Support\Str;
use App\Classes\Protocol as BaseProtocol; use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient; use App\Classes\Sock\SocketClient;
use App\Http\Controllers\DomainController; use App\Models\{Address,Domain,Mailer};
use App\Models\{Address,Domain};
/** /**
* Respond to DNS queries and provide addresses to FTN nodes. * Respond to DNS queries and provide addresses to FTN nodes.
* http://ftsc.org/docs/fts-5004.001
* *
* This implementation doesnt support EDNS nor DNSSEC. * This implementation doesnt support EDNS nor DNSSEC.
* *
@ -59,8 +59,8 @@ final class DNS extends BaseProtocol
public const DNS_TYPE_SOA = 6; // SOA Records public const DNS_TYPE_SOA = 6; // SOA Records
public const DNS_TYPE_MX = 15; // MX Records public const DNS_TYPE_MX = 15; // MX Records
public const DNS_TYPE_TXT = 16; // TXT Records public const DNS_TYPE_TXT = 16; // TXT Records
public const DNS_TYPE_AAAA = 28; // AAAA Records public const DNS_TYPE_AAAA = 28; // AAAA Records
public const DNS_TYPE_SRV = 33; // SRV Records
public const DNS_TYPE_OPT = 41; // OPT Records public const DNS_TYPE_OPT = 41; // OPT Records
public const DNS_TYPE_DS = 43; // DS Records (Delegation signer RFC 4034) public const DNS_TYPE_DS = 43; // DS Records (Delegation signer RFC 4034)
@ -70,7 +70,7 @@ final class DNS extends BaseProtocol
if (! parent::onConnect($client)) { if (! parent::onConnect($client)) {
Log::withContext(['pid'=>getmypid()]); Log::withContext(['pid'=>getmypid()]);
$this->setClient($client); $this->client = $client;
$this->protocol_session(); $this->protocol_session();
Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote)); Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote));
@ -90,6 +90,7 @@ final class DNS extends BaseProtocol
* Handle a DNS query * Handle a DNS query
* *
* https://www.ietf.org/rfc/rfc1035.txt * https://www.ietf.org/rfc/rfc1035.txt
* https://www.ietf.org/rfc/rfc2308.txt
* https://github.com/guyinatuxedo/dns-fuzzer/blob/master/dns.md * https://github.com/guyinatuxedo/dns-fuzzer/blob/master/dns.md
* *
* labels 63 octets or less * labels 63 octets or less
@ -97,21 +98,29 @@ final class DNS extends BaseProtocol
* TTL positive values of a signed 32 bit number. * TTL positive values of a signed 32 bit number.
* UDP messages 512 octets or less * UDP messages 512 octets or less
* *
* @param bool $force_queue Not used here
* @return int * @return int
* @throws \Exception * @throws \Exception
*/ */
public function protocol_session(): int public function protocol_session(bool $force_queue=FALSE): int
{ {
Log::debug(sprintf('%s:+ DNS Query',self::LOGKEY)); Log::debug(sprintf('%s:+ DNS Query',self::LOGKEY));
try {
$this->query = new BaseProtocol\DNS\Query($this->client->read(0,512)); $this->query = new BaseProtocol\DNS\Query($this->client->read(0,512));
} catch (\Exception $e) {
Log::error(sprintf('%s:! Ignoring bad DNS query (%s)',self::LOGKEY,$e->getMessage()));
return FALSE;
}
Log::info(sprintf('%s:= DNS Query from [%s] for [%s]',self::LOGKEY,$this->client->address_remote,$this->query->domain)); Log::info(sprintf('%s:= DNS Query from [%s] for [%s]',self::LOGKEY,$this->client->address_remote,$this->query->domain));
// If the wrong class // If the wrong class
if ($this->query->class !== self::DNS_QUERY_IN) { if ($this->query->class !== self::DNS_QUERY_IN) {
Log::error(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$this->query->class)); Log::error(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$this->query->class));
return $this->reply(self::DNS_NOTIMPLEMENTED); return $this->reply(self::DNS_NOTIMPLEMENTED,[],$this->soa());
} }
$dos = Domain::select(['id','name','dnsdomain'])->active(); $dos = Domain::select(['id','name','dnsdomain'])->active();
@ -140,17 +149,9 @@ final class DNS extends BaseProtocol
return $this->reply( return $this->reply(
self::DNS_NOERROR, self::DNS_NOERROR,
[serialize([ $this->soa(),
$this->domain_split(gethostname()),
$this->domain_split(Str::replace('@','.',config('app.mail.mail_from','nobody@'.gethostname()))),
1,
self::DEFAULT_TTL,
self::DEFAULT_TTL,
self::DEFAULT_TTL,
self::DEFAULT_TTL
]) => self::DNS_TYPE_SOA],
[], [],
[serialize($this->domain_split(gethostname())) => self::DNS_TYPE_NS], [serialize($this->domain_split(config('fido.dns_ns'))) => self::DNS_TYPE_NS],
); );
case self::DNS_TYPE_NS: case self::DNS_TYPE_NS:
@ -158,23 +159,52 @@ final class DNS extends BaseProtocol
return $this->reply( return $this->reply(
self::DNS_NOERROR, self::DNS_NOERROR,
[serialize($this->domain_split(gethostname())) => self::DNS_TYPE_NS]); [serialize($this->domain_split(config('fido.dns_ns'))) => self::DNS_TYPE_NS]);
// Respond to A/AAAA/CNAME queries, with value or NAMEERR // Respond to A/AAAA/CNAME queries, with value or NAMEERR
case self::DNS_TYPE_CNAME: case self::DNS_TYPE_CNAME:
case self::DNS_TYPE_A: case self::DNS_TYPE_A:
case self::DNS_TYPE_AAAA: case self::DNS_TYPE_AAAA:
case self::DNS_TYPE_SRV:
case self::DNS_TYPE_TXT:
Log::info(sprintf('%s:= Looking for record [%s] for [%s]',self::LOGKEY,$this->query->type,$this->query->domain)); Log::info(sprintf('%s:= Looking for record [%s] for [%s]',self::LOGKEY,$this->query->type,$this->query->domain));
$labels = clone($this->query->labels); $labels = clone($this->query->labels);
$mailer = '';
// If this is a SRV record query
if ($this->query->type === self::DNS_TYPE_SRV) {
if ($labels->skip(1)->first() !== '_tcp')
return $this->reply(self::DNS_NAMEERR);
switch ($labels->first()) {
case '_binkp':
$mailer = Mailer::where('name','BINKP')->singleOrFail();
break;
case '_ifcico':
$mailer = Mailer::where('name','EMSI')->singleOrFail();
break;
default:
return $this->reply(self::DNS_NAMEERR);
}
$labels->shift(2);
}
// First check that it is a query we can answer // First check that it is a query we can answer
// First label should be p.. or f.. // First label should be p.. or f..
if (! is_null($p=$this->parse('p',$labels->first()))) if (! is_null($p=$this->parse('p',$labels->first())))
$labels->shift(); $labels->shift();
else
$p = 0;
if (is_null($f=$this->parse('f',$labels->shift()))) // We'll assume f0
return $this->nameerr(); if (! is_null($f=$this->parse('f',$labels->first())))
$labels->shift();
else
$f = 0;
if (is_null($n=$this->parse('n',$labels->shift()))) if (is_null($n=$this->parse('n',$labels->shift())))
return $this->nameerr(); return $this->nameerr();
@ -200,22 +230,51 @@ final class DNS extends BaseProtocol
$ao = Address::findFTN(sprintf('%d:%d/%d.%d@%s',$z,$n,$f,$p,$d)); $ao = Address::findFTN(sprintf('%d:%d/%d.%d@%s',$z,$n,$f,$p,$d));
// Check we have the right record // Check we have the right record
if ((! $ao) || (! $ao->system->address) || (($rootdn !== self::TLD) && ((! $ao->zone->domain->dnsdomain) || ($ao->zone->domain->dnsdomain !== $rootdn)))) { if ((! $ao) || (($rootdn !== self::TLD) && ((! $ao->zone->domain->dnsdomain) || ($ao->zone->domain->dnsdomain !== $rootdn)))) {
Log::alert(sprintf('%s:= No DNS record for [%d:%d/%d.%d@%s]',self::LOGKEY,$z,$n,$f,$p,$d)); Log::alert(sprintf('%s:= No DNS record for [%d:%d/%d.%d@%s]',self::LOGKEY,$z,$n,$f,$p,$d));
return $this->nameerr(); return $this->nameerr();
} }
switch ($this->query->type) {
case self::DNS_TYPE_SRV:
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address,$ao->ftn)); Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address,$ao->ftn));
if (($ao->system->address) && ($xx=$ao->system->mailers->where('id',$mailer->id)->pop())) {
return $this->reply(
self::DNS_NOERROR,
[serialize([
0, // priority
1, // weight
$xx->pivot->port,
$this->domain_split($ao->system->address),
]) => self::DNS_TYPE_SRV]);
} else {
return $this->nodata();
}
case self::DNS_TYPE_TXT:
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->name,$ao->ftn));
return $this->reply( return $this->reply(
self::DNS_NOERROR,
[serialize($ao->system->name) => self::DNS_TYPE_TXT]);
default:
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address,$ao->ftn));
return (! $ao->system->address)
? $this->nodata()
: $this->reply(
self::DNS_NOERROR, self::DNS_NOERROR,
[serialize($this->domain_split($ao->system->address)) => self::DNS_TYPE_CNAME]); [serialize($this->domain_split($ao->system->address)) => self::DNS_TYPE_CNAME]);
}
// Other attributes return NOTIMPL // Other attributes return NOTIMPL
default: default:
Log::error(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$this->query->type)); Log::error(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$this->query->type));
return $this->reply(self::DNS_NOTIMPLEMENTED); return $this->reply(self::DNS_NOTIMPLEMENTED,[],$this->soa());
} }
} }
@ -230,11 +289,36 @@ final class DNS extends BaseProtocol
return pack('n',$offset | (3 << 14)); return pack('n',$offset | (3 << 14));
} }
/**
* Split a domain into a DNS domain string
*
* @param string $domain
* @return string
*/
private function domain_split(string $domain): string
{
$a = '';
foreach (explode('.',$domain) as $item)
$a .= pack('C',strlen($item)).$item;
$a .= "\x00";
return $a;
}
private function nameerr(): int private function nameerr(): int
{ {
Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$this->query->domain)); Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$this->query->domain));
return $this->reply(self::DNS_NAMEERR); return $this->reply(self::DNS_NAMEERR,[],$this->soa());
}
private function nodata(): int
{
Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s] in our zone(s)',self::LOGKEY,$this->query->domain));
return $this->reply(self::DNS_NOERROR,[],$this->soa());
} }
/** /**
@ -248,7 +332,7 @@ final class DNS extends BaseProtocol
{ {
$m = []; $m = [];
return (preg_match('/^'.$prefix.'([0-9]+)+/',$label,$m) && ($m[1] <= DomainController::NUMBER_MAX)) return (preg_match('/^'.$prefix.'([0-9]+)+/',$label,$m) && ($m[1] <= Address::ADDRESS_FIELD_MAX))
? $m[1] ? $m[1]
: NULL; : NULL;
} }
@ -320,24 +404,6 @@ final class DNS extends BaseProtocol
return TRUE; return TRUE;
} }
/**
* Split a domain into a DNS domain string
*
* @param string $domain
* @return string
*/
private function domain_split(string $domain): string
{
$a = '';
foreach (explode('.',$domain) as $item)
$a .= pack('C',strlen($item)).$item;
$a .= "\x00";
return $a;
}
/** /**
* Return a DNS Resource Record * Return a DNS Resource Record
* *
@ -378,10 +444,34 @@ final class DNS extends BaseProtocol
$a .= pack('NNNNN',$ars[2],$ars[3],$ars[4],$ars[5],$ars[6]); $a .= pack('NNNNN',$ars[2],$ars[3],$ars[4],$ars[5],$ars[6]);
break; break;
case self::DNS_TYPE_SRV:
$a .= pack('nnn',$ars[0],$ars[1],$ars[2]);
$a .= $ars[3];
break;
case self::DNS_TYPE_TXT:
$a .= pack('C',strlen($ars)).$ars;
break;
} }
$reply .= pack('n',strlen($a)).$a; $reply .= pack('n',strlen($a)).$a;
return $reply; return $reply;
} }
private function soa(): array
{
return
[serialize([
$this->domain_split(config('fido.dns_ns')),
$this->domain_split(Str::replace('@','.',config('app.mail.mail_from','nobody@'.gethostname()))),
1, // Serial
self::DEFAULT_TTL, // Refresh
self::DEFAULT_TTL, // Retry
self::DEFAULT_TTL*7,// Expire
self::DEFAULT_TTL // Minimum cache
]) => self::DNS_TYPE_SOA];
}
} }

View File

@ -32,7 +32,8 @@ final class Query
'arcount' => [0x05,'n',1], // Resource Records in the addition records section 'arcount' => [0x05,'n',1], // Resource Records in the addition records section
]; ];
public function __construct(string $buf) { public function __construct(string $buf)
{
$this->buf = $buf; $this->buf = $buf;
$rx_ptr = 0; $rx_ptr = 0;
@ -49,12 +50,20 @@ final class Query
$this->labels = collect(); $this->labels = collect();
while (($len=ord(substr($this->buf,$rx_ptr++,1))) !== 0x00) { while (($len=ord(substr($this->buf,$rx_ptr++,1))) !== 0x00) {
$this->labels->push(substr($this->buf,$rx_ptr,$len)); $this->labels->push(strtolower(substr($this->buf,$rx_ptr,$len)));
$rx_ptr += $len; $rx_ptr += $len;
} }
// Get the query type/class // Get the query type/class
try {
$result = unpack('ntype/nclass',substr($this->buf,$rx_ptr,4)); $result = unpack('ntype/nclass',substr($this->buf,$rx_ptr,4));
} catch (\Exception $e) {
Log::error(sprintf('%s:! Unpack failed: Buffer: [%s] (%d), RXPTR [%d]',self::LOGKEY,hex_dump($this->buf),strlen($this->buf),$rx_ptr));
throw $e;
}
$rx_ptr += 4; $rx_ptr += 4;
$this->type = $result['type']; $this->type = $result['type'];
$this->class = $result['class']; $this->class = $result['class'];
@ -65,17 +74,16 @@ final class Query
if ($this->arcount) { if ($this->arcount) {
// Additional records, EDNS: https://datatracker.ietf.org/doc/html/rfc6891 // Additional records, EDNS: https://datatracker.ietf.org/doc/html/rfc6891
if (($haystack = strstr(substr($this->buf,$rx_ptr+1+10),"\x00",true)) !== FALSE) { if (($haystack = strstr(substr($this->buf,$rx_ptr+1+10),"\x00",true)) !== FALSE) {
Log::error(sprintf('%s:! DNS additional record format error?',self::LOGKEY)); Log::error(sprintf('%s:! DNS additional record format error?',self::LOGKEY),['buf'=>hex_dump($this->buf)]);
// @todo catch this return;
} }
$this->additional = new RR(substr($this->buf,$rx_ptr,(strlen($haystack) === 0) ? NULL : strlen($haystack))); $this->additional = new RR(substr($this->buf,$rx_ptr,(strlen($haystack) === 0) ? NULL : strlen($haystack)));
$rx_ptr += $this->additional->length; $rx_ptr += $this->additional->length;
} }
if (strlen($this->buf) !== $rx_ptr) { if (strlen($this->buf) !== $rx_ptr)
dd(['query remaining'=>strlen($this->buf)-$rx_ptr,'hex'=>hex_dump(substr($this->buf,$rx_ptr))]); throw new \Exception(sprintf('! DNS Buffer still has [%d]: %s',strlen($this->buf)-$rx_ptr,hex_dump(substr($this->buf,$rx_ptr))));
}
} }
public function __get($key) public function __get($key)
@ -96,7 +104,8 @@ final class Query
} }
} }
public static function header_len() { public static function header_len()
{
return collect(self::header)->sum(function($item) { return $item[2]*2; }); return collect(self::header)->sum(function($item) { return $item[2]*2; });
} }

View File

@ -4,6 +4,7 @@ namespace App\Classes\Protocol\DNS;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use App\Classes\Protocol\DNS; use App\Classes\Protocol\DNS;
@ -26,8 +27,15 @@ final class RR
$domain = strstr($buf,"\x00",TRUE); $domain = strstr($buf,"\x00",TRUE);
$i += strlen($domain)+1; $i += strlen($domain)+1;
try {
$this->type = Arr::get(unpack('n',substr($buf,$i,2)),1); $this->type = Arr::get(unpack('n',substr($buf,$i,2)),1);
$this->class = Arr::get(unpack('n',substr($buf,$i+2,2)),1); $this->class = Arr::get(unpack('n',substr($buf,$i+2,2)),1);
} catch (\ErrorException $e) {
Log::error(sprintf('%s:! Error unpacking buffer [%s]',self::LOGKEY,$buf),['buf'=>hex_dump($buf)]);
return;
}
$i += 4; $i += 4;
switch ($this->type) { switch ($this->type) {

View File

@ -3,13 +3,13 @@
namespace App\Classes\Protocol; namespace App\Classes\Protocol;
use Carbon\Carbon; use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Classes\Protocol as BaseProtocol; use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient; use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException; use App\Classes\Sock\SocketException;
use App\Models\{Address,Setup}; use App\Exceptions\InvalidFTNException;
use App\Models\{Address,Mailer,Setup};
use App\Interfaces\CRC as CRCInterface; use App\Interfaces\CRC as CRCInterface;
use App\Interfaces\Zmodem as ZmodemInterface; use App\Interfaces\Zmodem as ZmodemInterface;
use App\Traits\CRC as CRCTrait; use App\Traits\CRC as CRCTrait;
@ -88,7 +88,6 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
* @param SocketClient $client * @param SocketClient $client
* @return int|null * @return int|null
* @throws SocketException * @throws SocketException
* @throws Exception
*/ */
public function onConnect(SocketClient $client): ?int public function onConnect(SocketClient $client): ?int
{ {
@ -96,8 +95,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
if (! parent::onConnect($client)) { if (! parent::onConnect($client)) {
Log::withContext(['pid'=>getmypid()]); Log::withContext(['pid'=>getmypid()]);
// @todo Can this be SESSION_EMSI? if so, set an object class value that in EMSI of SESSION_EMSI, and move this method to the parent class $this->session(Mailer::where('name','EMSI')->singleOrFail(),$client,(new Address));
$this->session(self::SESSION_AUTO,$client,(new Address));
$this->client->close(); $this->client->close();
exit(0); exit(0);
} }
@ -108,7 +106,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
/** /**
* Send our welcome banner * Send our welcome banner
* *
* @throws Exception * @throws \Exception
*/ */
private function emsi_banner(): void private function emsi_banner(): void
{ {
@ -123,7 +121,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
* Create the EMSI_DAT * Create the EMSI_DAT
* *
* @return string * @return string
* @throws Exception * @throws \Exception
*/ */
private function emsi_makedat(): string private function emsi_makedat(): string
{ {
@ -273,7 +271,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
* *
* @param string $str * @param string $str
* @return int * @return int
* @throws Exception * @throws \Exception
*/ */
private function emsi_parsedat(string $str): int private function emsi_parsedat(string $str): int
{ {
@ -321,17 +319,38 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
Log::debug(sprintf('%s: - Parsing AKA [%s]',self::LOGKEY,$rem_aka)); Log::debug(sprintf('%s: - Parsing AKA [%s]',self::LOGKEY,$rem_aka));
try { try {
if (! ($o = Address::findFTN($rem_aka))) { if (! ($o = Address::findFTN($rem_aka,TRUE))) {
Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka)); Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));
$this->node->ftn_other = $rem_aka; $this->node->ftn_other = $rem_aka;
continue; continue;
// If we only present limited AKAs dont validate password against akas outside of the domains we present
} elseif (is_null(our_address($o))) {
Log::alert(sprintf('%s:/ AKA domain [%s] is not in our domain(s) [%s] - ignoring',self::LOGKEY,$o->zone->domain->name,our_address()->pluck('zone.domain.name')->unique()->join(',')));
$this->node->ftn_other = $rem_aka;
continue;
} elseif (! $o->active) {
Log::alert(sprintf('%s:/ AKA is not active [%s] - ignoring',self::LOGKEY,$rem_aka));
$this->node->ftn_other = $rem_aka;
continue;
} else {
Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka));
} }
} catch (Exception) { } catch (InvalidFTNException $e) {
Log::error(sprintf('%s: ! AKA is INVALID [%s]',self::LOGKEY,$rem_aka)); Log::error(sprintf('%s:! AKA is INVALID [%s] (%s), ignoring',self::LOGKEY,$rem_aka,$e->getMessage()));
continue; continue;
} catch (\Exception) {
Log::error(sprintf('%s: ! AKA is INVALID [%s]',self::LOGKEY,$rem_aka));
return self::S_FAILURE|self::S_ADDTRY;
} }
// Check if the remote has our AKA // Check if the remote has our AKA
@ -504,7 +523,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
* STEP 2A, RECEIVE EMSI HANDSHAKE * STEP 2A, RECEIVE EMSI HANDSHAKE
* *
* @throws SocketException * @throws SocketException
* @throws Exception * @throws \Exception
*/ */
private function emsi_recv(int $mode): int private function emsi_recv(int $mode): int
{ {
@ -683,7 +702,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
* STEP 2B, TRANSMIT EMSI HANDSHAKE * STEP 2B, TRANSMIT EMSI HANDSHAKE
* *
* @throws SocketException * @throws SocketException
* @throws Exception * @throws \Exception
*/ */
private function emsi_send(): int private function emsi_send(): int
{ {
@ -822,7 +841,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
* STEP 1, EMSI INIT * STEP 1, EMSI INIT
* *
* @throws SocketException * @throws SocketException
* @throws Exception * @throws \Exception
*/ */
protected function protocol_init(): int protected function protocol_init(): int
{ {
@ -969,13 +988,14 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
* Setup our EMSI session * Setup our EMSI session
* *
* @return int * @return int
* @throws Exception * @throws \Exception
*/ */
protected function protocol_session(): int protected function protocol_session(bool $force_queue=FALSE): int
{ {
// @todo introduce emsi_init() to perform the job of protocol_init. Only needs to be done when we originate a session // @todo introduce emsi_init() to perform the job of protocol_init. Only needs to be done when we originate a session
Log::debug(sprintf('%s:+ Starting EMSI Protocol SESSION',self::LOGKEY)); Log::debug(sprintf('%s:+ Starting EMSI Protocol SESSION',self::LOGKEY));
$this->force_queue = $force_queue;
$was_req = 0; $was_req = 0;
$got_req = 0; $got_req = 0;
@ -1184,7 +1204,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
Log::debug(sprintf('%s:+ Start WAZOO Receive',self::LOGKEY)); Log::debug(sprintf('%s:+ Start WAZOO Receive',self::LOGKEY));
// @todo If the node is not defined in the DB node->address is NULL. Need to figure out how to handle those nodes. // @todo If the node is not defined in the DB node->address is NULL. Need to figure out how to handle those nodes.
$rc = (new Zmodem)->zmodem_receive($this->client,$zap,$this->recv,$this->node->address); $rc = (new Zmodem)->zmodem_receive($this->client,$zap,$this->recv,$this->node->address,$this->force_queue);
return ($rc === self::RCDO || $rc === self::ERROR); return ($rc === self::RCDO || $rc === self::ERROR);
} }
@ -1194,7 +1214,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
* *
* @param int $zap * @param int $zap
* @return bool * @return bool
* @throws Exception * @throws \Exception
*/ */
private function wazoosend(int $zap): bool private function wazoosend(int $zap): bool
{ {

View File

@ -10,7 +10,7 @@ use App\Classes\File\{Receive,Send};
use App\Classes\Sock\{SocketClient,SocketException}; use App\Classes\Sock\{SocketClient,SocketException};
use App\Interfaces\CRC as CRCInterface; use App\Interfaces\CRC as CRCInterface;
use App\Interfaces\Zmodem as ZmodemInterface; use App\Interfaces\Zmodem as ZmodemInterface;
use App\Models\Address; use App\Models\{Address,Mailer};
use App\Traits\CRC as CRCTrait; use App\Traits\CRC as CRCTrait;
/** /**
@ -213,7 +213,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
if (! parent::onConnect($client)) { if (! parent::onConnect($client)) {
Log::withContext(['pid'=>getmypid()]); Log::withContext(['pid'=>getmypid()]);
$this->session(self::SESSION_ZMODEM,$client); $this->session(Mailer::where('name','ZMODEM')->singleOrFail(),$client);
$this->client->close(); $this->client->close();
Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote)); Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote));
@ -273,10 +273,11 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
/** /**
* Setup our ZMODEM session * Setup our ZMODEM session
* *
* @param bool $force_queue Not used here
* @return int * @return int
* @throws \Exception * @throws \Exception
*/ */
public function protocol_session(): int public function protocol_session(bool $force_queue=FALSE): int
{ {
$proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK); $proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK);
@ -301,7 +302,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
* @param int $canzap * @param int $canzap
* @return int * @return int
*/ */
public function zmodem_receive(SocketClient $client,int $canzap,Receive $recv,Address $ao): int public function zmodem_receive(SocketClient $client,int $canzap,Receive $recv,Address $ao,bool $force_queue=FALSE): int
{ {
Log::debug(sprintf('%s:+ Starting ZModem Receive [%d]',self::LOGKEY,$canzap)); Log::debug(sprintf('%s:+ Starting ZModem Receive [%d]',self::LOGKEY,$canzap));
@ -363,7 +364,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
case self::FOP_CONT: case self::FOP_CONT:
case self::FOP_OK: case self::FOP_OK:
Log::info(sprintf('%s: = zmodem_receive Receving [%s] from [%d]',self::LOGKEY,$this->recv->nameas,$this->recv->pos)); Log::info(sprintf('%s:= zmodem_receive Receiving [%s] from [%d]',self::LOGKEY,$this->recv->nameas,$this->recv->pos));
$frame = self::ZRINIT; $frame = self::ZRINIT;
switch (($rc=$this->ls_zrecvfile($recv->pos))) { switch (($rc=$this->ls_zrecvfile($recv->pos))) {
@ -1489,7 +1490,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
$filesleft = -1; $filesleft = -1;
} else { } else {
$this->recv->new($file,$ao); $this->recv->new($file,$ao,$this->force_queue);
} }
return self::ZFILE; return self::ZFILE;

View File

@ -52,6 +52,99 @@ final class SocketClient {
if ($this->type === SOCK_STREAM) { if ($this->type === SOCK_STREAM) {
socket_getsockname($connection,$this->address_local,$this->port_local); socket_getsockname($connection,$this->address_local,$this->port_local);
socket_getpeername($connection,$this->address_remote,$this->port_remote); socket_getpeername($connection,$this->address_remote,$this->port_remote);
// If HAPROXY is used, work get the clients address
if (config('fido.haproxy')) {
Log::debug(sprintf('%s:+ HAPROXY connection host [%s] on port [%d] (%s)',self::LOGKEY,$this->address_remote,$this->port_remote,$this->type));
if ($this->read(5,12) !== "\x0d\x0a\x0d\x0a\x00\x0d\x0aQUIT\x0a") {
Log::error(sprintf('%s:! Failed to initialise HAPROXY connection',self::LOGKEY));
throw new SocketException(SocketException::CANT_CONNECT,'Failed to initialise HAPROXY connection');
}
// Version/Command
$vc = $this->read_ch(5);
if (($x=($vc>>4)&0x7) !== 2) {
Log::error(sprintf('%s:! HAPROXY version [%d] is not handled',self::LOGKEY,$x));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY version');
}
switch ($x=($vc&0x7)) {
// HAPROXY internal
case 0:
Log::debug(sprintf('%s:! HAPROXY internal health-check',self::LOGKEY));
throw new SocketException(SocketException::CANT_CONNECT,'Healthcheck');
// PROXY connection
case 1:
break;
default:
Log::error(sprintf('%s:! HAPROXY command [%d] is not handled',self::LOGKEY,$x));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY command');
}
// Protocol/Address Family
$pa = $this->read_ch(5);
$p = NULL;
switch ($x=($pa>>4)&0x7) {
case 1: // AF_INET
$p = 4;
break;
case 2: // AF_INET6
$p = 6;
break;
default:
Log::error(sprintf('%s:! HAPROXY protocol [%d] is not handled',self::LOGKEY,$x));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY protocol');
}
switch ($x=($pa&0x7)) {
case 1: // STREAM
break;
default:
Log::error(sprintf('%s:! HAPROXY address family [%d] is not handled',self::LOGKEY,$x));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY address family');
}
$len = Arr::get(unpack('n',$this->read(5,2)),1);
// IPv4
if (($p === 4) && ($len === 12)) {
$src = inet_ntop($this->read(5,4));
$dst = inet_ntop($this->read(5,4));
} elseif (($p === 6) && ($len === 36)) {
$src = inet_ntop($this->read(5,16));
$dst = inet_ntop($this->read(5,16));
} else {
Log::error(sprintf('%s:! HAPROXY address len [%d:%d] is not handled',self::LOGKEY,$p,$len));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY address length');
}
$src_port = unpack('n',$this->read(5,2));
$dst_port = unpack('n',$this->read(5,2));
$this->address_remote = $src;
$this->port_remote = Arr::get($src_port,1);
Log::info(sprintf('%s:! HAPROXY src [%s:%d] dst [%s:%d]',
self::LOGKEY,
$this->address_remote,
$this->port_remote,
$dst,
Arr::get($dst_port,1),
));
}
Log::info(sprintf('%s:+ Connection host [%s] on port [%d] (%s)',self::LOGKEY,$this->address_remote,$this->port_remote,$this->type)); Log::info(sprintf('%s:+ Connection host [%s] on port [%d] (%s)',self::LOGKEY,$this->address_remote,$this->port_remote,$this->type));
} }
} }
@ -108,6 +201,12 @@ final class SocketClient {
$sort = collect(['AAAA','A']); $sort = collect(['AAAA','A']);
if (filter_var($address,FILTER_VALIDATE_IP))
$resolved = collect([[
(($x=filter_var($address,FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) ? 'ipv6' : 'ip')=>$address,
'type'=>$x ? 'AAAA' : 'A'
]]);
else
// We only look at AAAA/A records // We only look at AAAA/A records
$resolved = collect(dns_get_record($address,DNS_AAAA|DNS_A)) $resolved = collect(dns_get_record($address,DNS_AAAA|DNS_A))
->filter(function($item) use ($sort) { return $sort->search(Arr::get($item,'type')) !== FALSE; }) ->filter(function($item) use ($sort) { return $sort->search(Arr::get($item,'type')) !== FALSE; })
@ -320,7 +419,7 @@ final class SocketClient {
} }
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error(sprintf('%s: - socket_recv Exception [%s]',self::LOGKEY,$e->getMessage())); Log::error(sprintf('%s:! socket_recv Exception [%s]',self::LOGKEY,$e->getMessage()));
throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x)); throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x));
} }

View File

@ -128,8 +128,9 @@ final class SocketServer {
try { try {
$r = new SocketClient($accept); $r = new SocketClient($accept);
} catch (\ErrorException $e) { } catch (\Exception $e) {
Log::error(sprintf('%s:! Creating Socket client failed? [%s]',self::LOGKEY,$e->getMessage())); Log::error(sprintf('%s:! Creating Socket client failed? [%s]',self::LOGKEY,$e->getMessage()));
socket_close($accept);
continue; continue;
} }

View File

@ -0,0 +1,37 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\AddressIdle as Job;
use App\Models\{Address,Domain};
class AddressIdle extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'address:idle'
.' {domain : Domain}'
.' {--ftn= : Limit to specific address}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Find and mark nodes as hold/down/delist if idle';
/**
* Execute the console command.
*/
public function handle(): int
{
$do = Domain::where('name',$this->argument('domain'))->singleOrFail();
return Job::dispatchSync($do,$this->option('ftn') ? Address::findFTN($this->option('ftn')) : NULL);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Console\Commands\Areafix;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Models\{Address,Echoarea,Echomail};
class Rescan extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'areafix:rescan {ftn} {area} {days?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Resend some echomail to a node';
/**
* Execute the console command.
*
* @return int
* @throws \Exception
*/
public function handle(): int
{
if (($this->argument('days')) && (! is_numeric($this->argument('days'))))
throw new \Exception('Days must be numeric: '.$this->argument('days'));
$ao = Address::findFtn($this->argument('ftn'));
if (! $ao)
throw new \Exception('FTN not found: '.$this->argument('ftn'));
// Check that the area belongs to the domain for the FTN
if (! $this->argument('area'))
throw new \Exception('Areaname is required');
$eao = Echoarea::where('name',$this->argument('area'))->singleOrFail();
if ($eao->domain_id !== $ao->zone->domain_id)
throw new \Exception(sprintf('Echo area [%s] is not in domain [%s] for FTN [%s]',$eao->name,$ao->zone->domain->name,$ao->ftn));
// Check that the user is subscribed
if (! $ao->echoareas->contains($eao->id))
throw new \Exception(sprintf('FTN [%s] is not subscribed to [%s]',$ao->ftn,$eao->name));
// Check that an FTN can read the area
if (! $eao->can_read($ao->security))
throw new \Exception(sprintf('FTN [%s] doesnt have permission to receive [%s]',$ao->ftn,$eao->name));
foreach (Echomail::select('id')
->where('echoarea_id',$eao->id)
->when($this->argument('days'),function($query) {
return $query->where('created_at','>=',Carbon::now()->subDays($this->argument('days'))->startOfDay());
})
->orderBy('datetime')
->cursor() as $eo) {
// Echomail hasnt been exported before
if (! $eo->seenby->count()) {
$eo->seenby()->attach($ao->id,['export_at'=>Carbon::now()]);
$this->info(sprintf('Exported [%d] to [%s]',$eo->id,$ao->ftn3d));
} else {
$export = $eo->seenby->where('id',$ao->id)->pop();
// Echomail is pending export
if ($export && $export->pivot->export_at && is_null($export->pivot->sent_at) && is_null($export->pivot->sent_pkt)) {
$this->warn(sprintf('Not exporting [%d] already queued for [%s]',$eo->id,$ao->ftn3d));
// Echomail has been exported
} elseif ($export) {
$eo->seenby()->updateExistingPivot($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL,'sent_pkt'=>NULL]);
$this->info(sprintf('Re-exported [%d] to [%s]',$eo->id,$ao->ftn3d));
// Echomail has not been exported
} else {
$eo->seenby()->attach($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL,'sent_pkt'=>NULL]);
$this->info(sprintf('Exported [%d] to [%s]',$eo->id,$ao->ftn3d));
}
}
}
return self::SUCCESS;
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands\BBS;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -13,7 +13,7 @@ class ANSIDecode extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'ansi:decode' protected $signature = 'bbs:ansi:decode'
.' {file : ANS file to decode}'; .' {file : ANS file to decode}';
/** /**
@ -26,10 +26,12 @@ class ANSIDecode extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
*/ */
public function handle() public function handle(): int
{ {
echo ANSI::ansi($this->argument('file')); echo ANSI::ansi($this->argument('file'));
return self::SUCCESS;
} }
} }

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands\BBS;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -13,7 +13,7 @@ class ANSIEncode extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'ansi:encode' protected $signature = 'bbs:ansi:encode'
.' {file : ANS file to encode}'; .' {file : ANS file to encode}';
/** /**
@ -26,9 +26,9 @@ class ANSIEncode extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
*/ */
public function handle() public function handle(): int
{ {
foreach (ANSI::binary($this->argument('file')) as $line) { foreach (ANSI::binary($this->argument('file')) as $line) {
foreach (str_split(bin2hex($line),2) as $y) foreach (str_split(bin2hex($line),2) as $y)
@ -36,5 +36,7 @@ class ANSIEncode extends Command
echo "\r"; echo "\r";
} }
return self::SUCCESS;
} }
} }

View File

@ -0,0 +1,104 @@
<?php
namespace App\Console\Commands\BBS;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Models\BBS\{Frame,Mode};
use Illuminate\Support\Arr;
class FrameImport extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bbs:frame:import {frame} {file} '.
'{--index=a : The frame index }'.
'{--access=0 : Is frame accessible }'.
'{--public=0 : Is frame limited to CUG }'.
'{--cost=0 : Frame Cost }'.
'{--mode=Ansi : Frame Emulation Mode }'.
'{--replace : Replace existing frame}'.
'{--type=i : Frame Type}'.
'{--title= : Frame Title}'.
'{--keys= : Key Destinations [0,1,2,3,4,5,6,7,8,9]}'.
'{--trim= : Trim off header (first n chars)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import frames into the database. The frames should be in binary format.';
/**
* Execute the console command.
*
* @return mixed
* @throws \Exception
*/
public function handle()
{
if (! is_numeric($this->argument('frame')))
throw new \Exception('Frame is not numeric: '.$this->argument('frame'));
if ((strlen($this->option('index')) !== 1) || (! preg_match('/^[a-z]$/',$this->option('index'))))
throw new \Exception('Subframe failed validation');
if (! file_exists($this->argument('file')))
throw new \Exception('File not found: '.$this->argument('file'));
$mo = Mode::where('name',$this->option('mode'))->firstOrFail();
$o = new Frame;
if ($this->option('replace')) {
try {
$o = $o->where('frame',$this->argument('frame'))
->where('index',$this->option('index'))
->where('mode_id',$mo->id)
->orderBy('created_at','DESC')
->firstOrNew();
} catch (ModelNotFoundException $e) {
$this->error('Frame not found to replace: '.$this->argument('frame').$this->option('index'));
exit(1);
}
}
$o->frame = $this->argument('frame');
$o->index = $this->option('index');
$o->mode_id = $mo->id;
$o->access = $this->option('access');
$o->public = $this->option('public');
$o->cost = $this->option('cost');
$o->type = $this->option('type');
$o->title = $this->option('title');
$keys = [];
if ($this->option('keys'))
$keys = explode(',',$this->option('keys'));
foreach (range(0,9) as $key) {
$index = sprintf('r%d',$key);
$o->{$index} = (($x=Arr::get($keys,$key,NULL)) === "null") ? NULL : $x;
}
// We need to escape any back slashes, so they dont get interpretted as hex
$o->content = $this->option('trim')
? substr(file_get_contents($this->argument('file')),$this->option('trim'))
: file_get_contents($this->argument('file'));
// If we have 0x1aSAUCE, we'll discard the sauce.
if ($x = strpos($o->content,chr(0x1a).'SAUCE')) {
$o->content = substr($o->content,0,$x-1).chr(0x0a);
}
$o->save();
$this->info(sprintf('Saved frame: [%s] as [%s] with [%d]',$o->page,$mo->name,$o->id));
}
}

View File

@ -0,0 +1,128 @@
<?php
/**
* Viewdata/Videotex Server
*
* Inspired by Rob O'Donnell at irrelevant.com
*/
namespace App\Console\Commands\BBS;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Classes\BBS\Server\{Ansitex,Videotex};
use App\Classes\Sock\{SocketException,SocketServer};
use App\Models\Mode;
use App\Models\Setup;
class Start extends Command
{
private const LOGKEY = 'CBS';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bbs:start {--mode=VideoTex : Server Mode Profile}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Start BBS Server';
/**
* Execute the console command.
*
* @return void
* @throws SocketException
*/
public function handle()
{
Log::channel('bbs')->info(sprintf('%s:+ BBS Server Starting (%d)',self::LOGKEY,getmypid()));
$o = Setup::findOrFail(config('app.id'));
$start = collect();
if (TRUE || $o->ansitex_active)
$start->put('ansitex',[
'address'=>$o->ansitex_bind,
'port'=>$o->ansitex_port,
'proto'=>SOCK_STREAM,
'class'=>new Ansitex,
]);
if (TRUE || $o->viewdata_active)
$start->put('videotex',[
'address'=>$o->videotex_bind,
'port'=>$o->videotex_port,
'proto'=>SOCK_STREAM,
'class'=>new Videotex,
]);
$children = collect();
Log::channel('bbs')->debug(sprintf('%s:# Servers [%d]',self::LOGKEY,$start->count()));
if (! $start->count()) {
Log::channel('bbs')->alert(sprintf('%s:! No servers configured to start',self::LOGKEY));
return;
}
//pcntl_signal(SIGCHLD,SIG_IGN);
foreach ($start as $item => $config) {
Log::channel('bbs')->debug(sprintf('%s:- Starting [%s] (%d)',self::LOGKEY,$item,getmypid()));
$pid = pcntl_fork();
if ($pid === -1)
die('could not fork');
// We are the child
if (! $pid) {
Log::channel('bbs')->withContext(['pid'=>getmypid()]);
Log::channel('bbs')->info(sprintf('%s:= Started [%s]',self::LOGKEY,$item));
$server = new SocketServer($config['port'],$config['address'],$config['proto']);
$server->handler = [$config['class'],'onConnect'];
try {
$server->listen();
} catch (SocketException $e) {
if ($e->getMessage() === 'Can\'t accept connections: "Success"')
Log::channel('bbs')->debug(sprintf('%s:! Server Terminated [%s]',self::LOGKEY,$item));
else
Log::channel('bbs')->emergency(sprintf('%s:! Uncaught Message: %s',self::LOGKEY,$e->getMessage()));
}
Log::channel('bbs')->info(sprintf('%s:= Finished: [%s]',self::LOGKEY,$item));
// Child finished we need to get out of this loop.
exit;
}
Log::channel('bbs')->debug(sprintf('%s:- Forked for [%s] (%d)',self::LOGKEY,$item,$pid));
$children->put($pid,$item);
}
// Wait for children to exit
while ($x=$children->count()) {
// Wait for children to finish
$exited = pcntl_wait($status);
if ($exited < 0)
abort(500,sprintf('Something strange for status: [%s] (%d)',pcntl_wifsignaled($status) ? pcntl_wtermsig($status) : 'unknown',$exited));
Log::channel('bbs')->info(sprintf('%s:= Exited: #%d [%s]',self::LOGKEY,$x,$children->pull($exited)));
}
// Done
Log::channel('bbs')->debug(sprintf('%s:= Finished.',self::LOGKEY));
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Classes\Sock\SocketException;
use App\Classes\Sock\SocketServer;
use App\Classes\Protocol\Zmodem as ZmodemClass;
class CommZmodemReceive extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'comm:zmodem:receive';
/**
* The console command description.
*
* @var string
*/
protected $description = 'ZMODEM receive';
/**
* Execute the console command.
*
* @return mixed
* @throws SocketException
*/
public function handle()
{
Log::info('Listening for ZMODEM connections...');
$server = new SocketServer(60177,'0.0.0.0');
$server->handler = [new ZmodemClass,'onConnect'];
try {
$server->listen();
} catch (SocketException $e) {
if ($e->getMessage() === 'Can\'t accept connections: "Success"')
Log::debug('Server Terminated');
else
Log::emergency('Uncaught Message: '.$e->getMessage());
}
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Classes\Protocol;
use App\Classes\Protocol\Zmodem as ZmodemClass;
use App\Classes\Sock\SocketClient;
class CommZmodemSend extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'comm:zmodem:send {ip}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'ZMODEM send';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
Log::info('Call ZMODEM send');
[$address,$service_port] = explode(':',$this->argument('ip'),2);
$client = SocketClient::create($address,$service_port);
$o = new ZmodemClass;
$o->session(Protocol::SESSION_ZMODEM,$client);
Log::info(sprintf('Connection ended: %s',$client->address_remote),['m'=>__METHOD__]);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands\Debug;
use Illuminate\Console\Command;
use App\Models\Address;
class AddressCheck extends Command
{
protected $signature = 'debug:address:check'
.' {ftn : FTN}';
protected $description = 'Check the addresses we use for a node';
public function handle(): int
{
$o = Address::findFTN($this->argument('ftn'));
if (! $o) {
$this->error(sprintf('Address: %s doesnt exist?',$this->argument('ftn')));
return Command::FAILURE;
}
$this->info(sprintf('Address: %s (%s)',$o->ftn,$o->role_name));
$this->info(sprintf("Children: \n- %s",$o->children()->pluck('ftn4d')->join("\n- ")));
$this->info(sprintf("Downstream: \n- %s",$o->downstream()->pluck('ftn4d')->join("\n- ")));
$this->info(sprintf('Uplink: %s (Parent: %s)',$o->uplink()?->ftn,$o->parent()?->ftn));
$this->info(sprintf('Our Address: %s',our_address($o)?->ftn));
$this->info(sprintf('- Domain Addresses: %s',our_address($o->zone->domain)->pluck('ftn4d')->join(',')));
return self::SUCCESS;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands\Debug;
use App\Models\Address;
use Illuminate\Console\Command;
class AddressCheckRole extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'debug:address:check:role {--f|fix : Fix the role}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check address roles and optionally fix';
/**
* Execute the console command.
*/
public function handle(): int
{
foreach (Address::withTrashed()->with(['zone.domain'])->cursor() as $o) {
// Trim the role bit from role, since we now work out a role automatically.
// @todo This doesnt work, because role_id returns back the overridden role, and thus would remove it
if (($o->role & Address::NODE_ALL) === $o->role_id) {
$o->role &= ~$o->role_id;
if ((! $o->role) || ($o->role === Address::NODE_UNKNOWN))
$o->role = NULL;
if ($o->getDirty())
if ($this->option('fix')) {
$o->save();
} else {
$this->warn(sprintf('Not changing [%s](%s) from [%d] to [%d]',$o->ftn,$o->role_name,$o->getOriginal('role'),$o->role));
}
}
}
return self::SUCCESS;
}
}

View File

@ -1,12 +1,12 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands\Debug;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use App\Models\Address; use App\Models\{Address,System};
class AddressMerge extends Command class AddressMerge extends Command
{ {
@ -15,7 +15,7 @@ class AddressMerge extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'address:merge' protected $signature = 'debug:address:merge'
.' {src : Source Address}' .' {src : Source Address}'
.' {dst : Destination Address}' .' {dst : Destination Address}'
.' {--F|force : Force}' .' {--F|force : Force}'
@ -34,79 +34,84 @@ class AddressMerge extends Command
* *
* @return int * @return int
*/ */
public function handle() public function handle(): int
{ {
$src = Address::withTrashed()->findOrfail($this->argument('src')); $src = Address::withTrashed()->findOrfail($this->argument('src'));
$dst = Address::withTrashed()->findOrfail($this->argument('dst')); $dst = Address::withTrashed()->findOrfail($this->argument('dst'));
if ((! $this->option('ignore')) && ($src->system_id !== $dst->system_id) && ($src->system->name !== 'Discovered System')) { if ((! $this->option('ignore')) && ($src->system_id !== $dst->system_id) && ($src->system->name !== System::default)) {
$this->error(sprintf('FTN addresses are from different systems (%s/%s)',$src->system->name,$dst->system->name)); $this->error(sprintf('FTN addresses are from different systems (%s/%s)',$src->system->name,$dst->system->name));
exit(1);
return self::FAILURE;
} }
if ((! $this->option('force')) && ($src->ftn !== $dst->ftn)) { if ((! $this->option('force')) && ($src->ftn !== $dst->ftn)) {
$this->error(sprintf('FTN addresses are not the same (%s:%s)',$src->ftn,$dst->ftn)); $this->error(sprintf('FTN addresses are not the same (%s:%s)',$src->ftn,$dst->ftn));
exit(1);
return self::FAILURE;
} }
if ($src->active) { if ($src->active) {
$this->error(sprintf('Source [%s] is still active',$src->ftn)); $this->error(sprintf('Source [%s] is still active',$src->ftn));
exit(1);
return self::FAILURE;
} }
DB::beginTransaction(); DB::beginTransaction();
// Find all echomail seenbys // Find all echomail seenbys
$x = DB::update('update echomail_seenby set address_id=? where address_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE echomail_seenby SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
$this->info(sprintf('Updated [%d] echomail seenby records',$x)); $this->info(sprintf('Updated [%d] echomail seenby records',$x));
// Find all echomail paths // Find all echomail paths
$x = DB::update('update echomail_path set address_id=? where address_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE echomail_path SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
$this->info(sprintf('Updated [%d] echomail path records',$x)); $this->info(sprintf('Updated [%d] echomail path records',$x));
// Find all echomails // Find all echomails
$x = DB::update('update echomails set fftn_id=? where fftn_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE echomails SET fftn_id=? WHERE fftn_id=?',[$dst->id,$src->id]);
$this->info(sprintf('Updated [%d] echomail source records',$x)); $this->info(sprintf('Updated [%d] echomail source records',$x));
// Find all netmails // Find all netmails
$x = DB::update('update netmails set fftn_id=? where fftn_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE netmails SET fftn_id=? WHERE fftn_id=?',[$dst->id,$src->id]);
$this->info(sprintf('Updated [%d] netmail source records',$x)); $this->info(sprintf('Updated [%d] netmail source records',$x));
// Find all netmails // Find all netmails
$x = DB::update('update netmails set tftn_id=? where tftn_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE netmails SET tftn_id=? WHERE tftn_id=?',[$dst->id,$src->id]);
$this->info(sprintf('Updated [%d] netmail destination records',$x)); $this->info(sprintf('Updated [%d] netmail destination records',$x));
// Find all nodelist // Find all nodelist
$x = DB::update('update address_nodelist set address_id=? where address_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE address_nodelist SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
$this->info(sprintf('Updated [%d] nodelist records',$x)); $this->info(sprintf('Updated [%d] nodelist records',$x));
// Find all file seenbys // Find all file seenbys
$x = DB::update('update file_seenby set address_id=? where address_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE file_seenby SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
$this->info(sprintf('Updated [%d] file seenby records',$x)); $this->info(sprintf('Updated [%d] file seenby records',$x));
// Find all files // Find all files
$x = DB::update('update files set fftn_id=? where fftn_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE files SET fftn_id=? WHERE fftn_id=?',[$dst->id,$src->id]);
$this->info(sprintf('Updated [%d] file source records',$x)); $this->info(sprintf('Updated [%d] file source records',$x));
// Resubscribe echoareas // Resubscribe echoareas
try { try {
$x = DB::update('update address_echoarea set address_id=? where address_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE address_echoarea SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
} catch (QueryException $e) { } catch (QueryException $e) {
DB::rollback(); DB::rollback();
$this->error(sprintf('You may need to remove %s:%s (%d) from echoareas',$src->ftn,$src->system->name,$src->id)); $this->error(sprintf('You may need to remove %s:%s (%d) from echoareas',$src->ftn,$src->system->name,$src->id));
exit(1);
return self::FAILURE;
} }
$this->info(sprintf('Updated [%d] echomail subscription records',$x)); $this->info(sprintf('Updated [%d] echomail subscription records',$x));
// Resubscribe fileareas // Resubscribe fileareas
try { try {
$x = DB::update('update address_filearea set address_id=? where address_id=?',[$dst->id,$src->id]); $x = DB::update('UPDATE address_filearea SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
} catch (QueryException $e) { } catch (QueryException $e) {
DB::rollback(); DB::rollback();
$this->error(sprintf('You may need to remove %s:%s (%d) from fileareas',$src->ftn,$src->system->name,$src->id)); $this->error(sprintf('You may need to remove %s:%s (%d) from fileareas',$src->ftn,$src->system->name,$src->id));
exit(1);
return self::FAILURE;
} }
$this->info(sprintf('Updated [%d] filearea subscription records',$x)); $this->info(sprintf('Updated [%d] filearea subscription records',$x));
@ -126,6 +131,6 @@ class AddressMerge extends Command
} }
} }
return Command::SUCCESS; return self::SUCCESS;
} }
} }

View File

@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands\Debug;
use Illuminate\Console\Command;
use App\Classes\File\Send;
use App\Classes\File\Send\Dynamic;
use App\Models\Address;
use App\Models\Dynamic as DynamicModel;
class DynamicItem extends Command
{
protected $signature = 'debug:dynamic:item'
.' {name : Dynamic Item}'
.' {ftn : FTN Address}';
protected $description = 'Generate a dynamic item';
public function handle(): int
{
$do = DynamicModel::where('name',$this->argument('name'))->single();
if (! $do)
throw new \Exception(sprintf('Dynamic Item [%s] doesnt exist?',$this->argument('name')));
$ao = Address::findFTN($this->argument('ftn'));
$d = new Dynamic($do,$ao,Send::T_FILE);
$d->open();
echo $d->read($d->size);
return self::SUCCESS;
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands\Debug;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -14,7 +14,7 @@ class EchomailDump extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'echomail:dump {id}'; protected $signature = 'debug:echomail:dump {id}';
/** /**
* The console command description. * The console command description.
@ -26,10 +26,12 @@ class EchomailDump extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
*/ */
public function handle() public function handle(): int
{ {
dump(Echomail::findOrFail($this->argument('id'))); dump(Echomail::findOrFail($this->argument('id')));
return self::SUCCESS;
} }
} }

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands\Debug;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
@ -15,7 +15,7 @@ class NetmailTest extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'netmail:test' protected $signature = 'debug:netmail:test'
.' {ftn : FTN to send netmail to}'; .' {ftn : FTN to send netmail to}';
/** /**
@ -28,13 +28,15 @@ class NetmailTest extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
* @throws \Exception * @throws \Exception
*/ */
public function handle() public function handle():int
{ {
$ao = Address::findFTN($this->argument('ftn')); $ao = Address::findFTN($this->argument('ftn'));
Notification::route('netmail',$ao)->notify(new NetmailTestNotification()); Notification::route('netmail',$ao)->notify(new NetmailTestNotification());
return self::SUCCESS;
} }
} }

View File

@ -0,0 +1,62 @@
<?php
namespace App\Console\Commands\Debug;
use Illuminate\Console\Command;
use App\Models\{Echomail,Netmail};
use App\Models\Address;
class PacketAddress extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'debug:packet:address'
.' {ftn : FTN Address}'
.' {type : Message Type}'
.' {dbid : Message ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate a mail packet for a system';
/**
* Execute the console command.
*
* @return int
* @throws \Exception
*/
public function handle(): int
{
$ao = Address::findFTN($this->argument('ftn'));
switch ($this->argument('type')) {
case 'echomail':
$o = new Echomail;
break;
case 'netmail':
$o = new Netmail;
break;
default:
$this->error('Unknown type: '.$this->argument('type'));
exit(1);
}
echo hex_dump($ao
->system
->packet($ao)
->mail($o->where('id',$this->argument('dbid'))->get())
->generate()
);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Console\Commands\Debug;
use Illuminate\Console\Command;
use App\Models\Address;
class PacketDump extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'debug:packet:dump'.
' {type : Type of packet, netmail|echomail }'.
' {ftn : FTN}'.
' {file? : filename}'.
' {--dump : Dump packet}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create an outgoing FTN packet';
/**
* Execute the console command.
*
* @return int
* @throws \Exception
*/
public function handle(): int
{
$ao = Address::findFTN($this->argument('ftn'));
switch (strtolower($this->argument('type'))) {
case 'netmail':
$pkt = $ao->getNetmail();
break;
case 'echomail':
$pkt = $ao->getEchomail();
break;
default:
$this->error('Unknown type: '.$this->argument('type'));
throw new \Exception('Unknown type: '.$this->argument('type'));
}
if ($this->option('dump')) {
$this->info('Item Name:'.$pkt->name);
$this->info('Item Type:'.get_class($pkt));
$this->info('Dump:');
echo hex_dump($pkt);
} else {
$f = fopen($this->argument('file'),'w+');
fputs($f,(string)$pkt);
fclose($f);
}
return self::SUCCESS;
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands\Debug;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
@ -15,7 +15,7 @@ class SendTestEmail extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'email:test {id}'; protected $signature = 'debug:email:test {id}';
/** /**
* The console command description. * The console command description.
@ -27,13 +27,15 @@ class SendTestEmail extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
*/ */
public function handle() public function handle(): int
{ {
$uo = User::find($this->argument('id')); $uo = User::find($this->argument('id'));
Mail::to($uo->email) Mail::to($uo->email)
->send(new MailTest($uo)); ->send(new MailTest($uo));
return self::SUCCESS;
} }
} }

View File

@ -0,0 +1,47 @@
<?php
namespace App\Console\Commands\Debug;
use Illuminate\Console\Command;
use App\Models\Domain;
class ZoneCheck extends Command
{
protected $signature = 'debug:zone:check'
.' {domain : Domain Name}'
.' {--Z|zone= : Zone}';
protected $description = 'Check that the addresses in a zone are configured correctly';
public function handle(): int
{
$do = Domain::where('name',$this->argument('domain'))->singleOrFail();
foreach ($do->zones->sortby('zone_id') as $zo) {
if ($this->option('zone') && ($this->option('zone') != $zo->zone_id))
continue;
$this->warn('Zone: '.$zo->zone_id);
$this->info(sprintf('- Our address(es): %s',our_address($do)->pluck('ftn4d')->join(',')));
$this->table(['id','ftn','role','parent','children','downlinks','uplink','send from','region_id','system','notes'],$zo->addresses()->FTNorder()->active()->with(['system'])->dontCache()->get()->transform(function($item) {
return [
'id'=>$item->id,
'ftn'=>$item->ftn4d,
'role'=>$item->role_name,
'parent'=>$item->parent()?->ftn4d,
'children'=>$item->children()->count(),
'downlinks'=>$item->downlinks()->count(),
'uplink'=>($x=$item->uplink())?->ftn4d,
'send from'=>$x ? our_address($item->uplink())?->ftn4d : '',
'region_id'=>$item->region_id,
'system'=>$item->system->name,
'notes'=>$item->isRoleOverride() ? 'Role Override' : '',
];
}));
}
return self::SUCCESS;
}
}

View File

@ -30,9 +30,9 @@ class EchoareaImport extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
*/ */
public function handle() public function handle(): int
{ {
$do = Domain::where('name',strtolower($this->argument('domain')))->singleOrFail(); $do = Domain::where('name',strtolower($this->argument('domain')))->singleOrFail();
return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix'),$this->option('unlink')); return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix'),$this->option('unlink'));

View File

@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Domain;
use App\Jobs\FileareaImport as Job;
class FileareaImport extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'filearea:import'
.' {file : NA File}'
.' {domain : Domain}'
.' {--P|prefix= : Add prefix to description}'
.' {--U|unlink : Delete file after import}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import Filearea';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$do = Domain::where('name',strtolower($this->argument('domain')))->singleOrFail();
return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix'),$this->option('unlink'));
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Console\Commands\Filefix;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Models\{Address,Filearea,File};
class Rescan extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'filefix:rescan {ftn} {area} {file?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Resend some files to a node';
/**
* Execute the console command.
*
* @return int
* @throws \Exception
*/
public function handle(): int
{
$ao = Address::findFtn($this->argument('ftn'));
if (! $ao)
throw new \Exception('FTN not found: '.$this->argument('ftn'));
// Check that the area belongs to the domain for the FTN
if (! $this->argument('area'))
throw new \Exception('Areaname is required');
$fao = Filearea::where('name',$this->argument('area'))->singleOrFail();
if ($fao->domain_id !== $ao->zone->domain_id)
throw new \Exception(sprintf('File area [%s] is not in domain [%s] for FTN [%s]',$fao->name,$ao->zone->domain->name,$ao->ftn));
// Check that the user is subscribed
if (! $ao->fileareas->contains($fao->id))
throw new \Exception(sprintf('FTN [%s] is not subscribed to [%s]',$ao->ftn,$fao->name));
// Check that an FTN can read the area
if (! $fao->can_read($ao->security))
throw new \Exception(sprintf('FTN [%s] doesnt have permission to receive [%s]',$ao->ftn,$fao->name));
foreach (File::select('id')
->where('filearea_id',$fao->id)
->when($this->argument('file'),function($query) {
return $query->where('name','=',$this->argument('days'));
})
->orderBy('datetime')
->cursor() as $fo) {
// File hasnt been exported before
if (! $fo->seenby->count()) {
$fo->seenby()->attach($ao->id,['export_at'=>Carbon::now()]);
$this->info(sprintf('Exported [%d] to [%s]',$fo->id,$ao->ftn3d));
} else {
$export = $fo->seenby->where('id',$ao->id)->pop();
// File is pending export
if ($export && $export->pivot->export_at && is_null($export->pivot->sent_at) && is_null($export->pivot->sent_pkt)) {
$this->warn(sprintf('Not exporting [%d] already queued for [%s]',$fo->id,$ao->ftn3d));
// File has been exported
} elseif ($export) {
$fo->seenby()->updateExistingPivot($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL]);
$this->info(sprintf('Re-exported [%d] to [%s]',$fo->id,$ao->ftn3d));
// File has not been exported
} else {
$fo->seenby()->attach($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL]);
$this->info(sprintf('Exported [%d] to [%s]',$fo->id,$ao->ftn3d));
}
}
}
return self::SUCCESS;
}
}

View File

@ -27,7 +27,7 @@ class FilesList extends Command
* *
* @return int * @return int
*/ */
public function handle() public function handle(): int
{ {
$this->table([ $this->table([
'files.id' => 'ID', 'files.id' => 'ID',
@ -37,6 +37,6 @@ class FilesList extends Command
->join('fileareas',['fileareas.id'=>'files.filearea_id']) ->join('fileareas',['fileareas.id'=>'files.filearea_id'])
->cursor()); ->cursor());
return Command::SUCCESS; return self::SUCCESS;
} }
} }

View File

@ -1,34 +0,0 @@
<?php
namespace App\Console\Commands;
use Database\Seeders\InitialSetupSeeder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class InitialSetup extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'initial:setup';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Initial Setup of DB';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
Artisan::call('db:seed',['class'=>InitialSetupSeeder::class]);
}
}

View File

@ -25,7 +25,7 @@ class JobList extends Command
/** /**
* Execute the console command. * Execute the console command.
*/ */
public function handle() public function handle(): int
{ {
$lastq = NULL; $lastq = NULL;
@ -35,16 +35,17 @@ class JobList extends Command
$lastq = $o->queue; $lastq = $o->queue;
} }
$this->info(sprintf('%s-%d: %s[%s] - %d tries (Created: %s,Timeout: %s,Next: %s)', $this->info(sprintf('%s-%d: %s[%s] - %d/%d tries [Next:%s]%s',
$o->uuid, $o->uuid,
$o->id, $o->id,
$o->display_name, $o->display_name,
$o->command->subject, $o->command->jobname,
$o->attempts, $o->attempts,$o->maxTries,
$o->created_at, $o->available_at ?: '-',
$o->retry_until ?: '-', $o->attempts ? sprintf(' (Created:%s)',$o->created_at) : ''
$o->reserved_at ?: '-',
)); ));
} }
return self::SUCCESS;
} }
} }

View File

@ -0,0 +1,79 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Address;
class MailList extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'mail:list {ftn : FTN address}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'List mail waiting for a node';
/**
* Execute the console command.
*
* @return int
* @throws \Exception
*/
public function handle(): int
{
$ao = Address::findFTN($this->argument('ftn'),TRUE);
if (! $ao) {
$this->error(sprintf('%s not found?',$this->argument('ftn')));
return self::FAILURE;
}
$this->info('Netmail');
$this->table([
'id' => 'ID',
'msgid' => 'MSGID',
'from' => 'FROM',
'to' => 'TO',
'subject' => 'SUBJECT',
],$ao->netmailWaiting()->map(function($item) {
return [
'id'=>$item->id,
'msgid'=>$item->msgid,
'from'=>$item->from,
'to'=>$item->to,
'subject'=>$item->subject,
];
}));
$this->info('Echomail');
$this->table([
'id' => 'ID',
'msgid' => 'MSGID',
'from' => 'FROM',
'to' => 'TO',
'subject' => 'SUBJECT',
'area' => 'AREA',
],$ao->echomailWaiting()->map(function($item) {
return [
'id'=>$item->id,
'msgid'=>$item->msgid,
'from'=>$item->from,
'to'=>$item->to,
'subject'=>$item->subject,
'area'=>$item->echoarea->name,
];
}));
return self::SUCCESS;
}
}

View File

@ -27,7 +27,7 @@ class MailSend extends Command
/** /**
* Execute the console command. * Execute the console command.
*/ */
public function handle() public function handle(): int
{ {
switch ($this->option('type')) { switch ($this->option('type')) {
case 'crash': case 'crash':
@ -48,5 +48,7 @@ class MailSend extends Command
default: default:
$this->error('Specify -T crash, normal or all'); $this->error('Specify -T crash, normal or all');
} }
return self::SUCCESS;
} }
} }

View File

@ -17,6 +17,7 @@ class NodelistImport extends Command
protected $signature = 'nodelist:import' protected $signature = 'nodelist:import'
.' {file : File ID | filename}' .' {file : File ID | filename}'
.' {domain? : Domain Name}' .' {domain? : Domain Name}'
.' {--I|ignorecrc : Ignore the CRC}'
.' {--D|delete : Delete old data for the date}' .' {--D|delete : Delete old data for the date}'
.' {--U|unlink : Delete file after import}' .' {--U|unlink : Delete file after import}'
.' {--T|test : Dry run}'; .' {--T|test : Dry run}';
@ -31,16 +32,26 @@ class NodelistImport extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
*/ */
public function handle() public function handle():int
{ {
try {
return Job::dispatchSync( return Job::dispatchSync(
is_numeric($x=$this->argument('file')) ? File::findOrFail($x) : $x, is_numeric($x=$this->argument('file'))
? File::findOrFail($x)
: sprintf('%s/%s',config('fido.dir'),$this->argument('file')),
$this->argument('domain'), $this->argument('domain'),
$this->option('delete'), $this->option('delete'),
$this->option('unlink'), $this->option('unlink'),
$this->option('test') $this->option('test'),
$this->option('ignorecrc'),
); );
} catch (\Exception $e) {
$this->error($e->getMessage());
return self::FAILURE;
}
} }
} }

View File

@ -3,10 +3,11 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use App\Classes\File; use App\Classes\File;
use App\Classes\FTN\Packet; use App\Classes\FTN\Packet;
use App\Models\System; use App\Models\{Address,Echomail};
class PacketInfo extends Command class PacketInfo extends Command
{ {
@ -17,7 +18,7 @@ class PacketInfo extends Command
*/ */
protected $signature = 'packet:info' protected $signature = 'packet:info'
.' {file : Packet to process}' .' {file : Packet to process}'
.' {system? : System the packet is from}'; .' {ftn? : FTN the packet is from}';
/** /**
* The console command description. * The console command description.
@ -29,41 +30,63 @@ class PacketInfo extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
* @throws \App\Classes\FTN\InvalidPacketException * @throws \App\Exceptions\InvalidPacketException
*/ */
public function handle() public function handle():int
{ {
$f = new File($this->argument('file')); $fs = Storage::disk(config('fido.local_disk'));
$s = $this->argument('system') ? System::where('name',$this->argument('system'))->singleOrFail() : NULL; $rel_name = sprintf('%s/%s',config('fido.dir'),$this->argument('file'));
$a = NULL;
$f = new File($fs->path($rel_name));
$m = NULL;
if ($this->argument('ftn')) {
$a = Address::findFTN($this->argument('ftn'));
} elseif (preg_match(sprintf('/^%s\.(.{3})$/',Packet::regex),$this->argument('file'),$m)) {
$a = Address::findOrFail(hexdec($m[1]));
}
foreach ($f as $packet) { foreach ($f as $packet) {
$pkt = Packet::process($packet,$x=$f->itemName(),$f->itemSize(),$s); $pkt = Packet::process($packet,$x=$f->itemName(),$f->itemSize(),$a?->zone->domain);
$this->alert(sprintf('File Name: %s',$x)); $this->alert(sprintf('File Name: %s',$x));
$this->info(sprintf('Packet Type : %s (%s)',$pkt->type,get_class($pkt))); $this->info(sprintf('Packet Type : %s (%s)',$pkt->type,get_class($pkt)));
$this->info(sprintf('From : %s to %s',$pkt->fftn,$pkt->tftn)); $this->info(sprintf('From : %s to %s',$pkt->fftn->ftn,$pkt->tftn ? $pkt->tftn->ftn : $pkt->tftn_t));
$this->info(sprintf('Dated : %s',$pkt->date)); $this->info(sprintf('Dated : %s (%s) [%s]',$pkt->date,$pkt->date->timestamp,$pkt->date->tz->toOffsetName()));
$this->info(sprintf('Password : %s (%s)',$pkt->password,$pkt->password ? 'SET' : 'NOT set')); $this->info(sprintf('Password : %s (%s)',$pkt->password ?: '-',$pkt->password ? 'SET' : 'NOT set'));
$this->info(sprintf('Messages : %d',$pkt->messages->count())); $this->info(sprintf('Messages : %d',$pkt->count()));
$this->info(sprintf('Tosser : %d (%s) version %s',$pkt->software->code,$pkt->software->name,$pkt->software_ver)); $this->info(sprintf('Tosser : %d (%s) version %s (%x)',$pkt->software->code,$pkt->software->name,$pkt->software_ver,$pkt->product));
$this->info(sprintf('Capabilities: %x',$pkt->capability)); $this->info(sprintf('Capabilities: %s',$pkt->capability));
$this->info(sprintf('Has Errors : %s',$pkt->errors->count() ? 'YES' : 'No')); $this->info(sprintf('Has Errors : %s',$pkt->errors->count() ? 'YES' : 'No'));
$this->info(sprintf('Messages : %d',$pkt->count())); $this->info(sprintf('Messages : %d',$pkt->count()));
foreach ($pkt as $msg) { foreach ($pkt as $msg) {
try { echo "\n";
$this->warn(sprintf('- Date : %s',$msg->date));
$this->warn(sprintf(' - Flags : %s',$msg->flags()->filter()->keys()->join(', ')));
$this->warn(sprintf(' - From : %s (%s)',$msg->user_from,$msg->fftn));
$this->warn(sprintf(' - To : %s (%s)',$msg->user_to,$msg->tftn));
$this->warn(sprintf(' - Subject: %s',$msg->subject));
$this->warn(sprintf(' - Area : %s',$msg->echoarea));
if ($msg->errors) try {
foreach ($msg->errors->errors()->all() as $error) $this->warn(sprintf('- Date : %s (%s)',$msg->datetime,$msg->datetime->tz->toOffsetName()));
$this->line(' - '.$error); $this->warn(sprintf(' - Errors : %s',$msg->errors->count() ? 'YES' : 'No'));
$this->warn(sprintf(' - Flags : %s',$msg->flags()->keys()->join(', ')));
$this->warn(sprintf(' - Cost : %d',$msg->cost));
$this->warn(sprintf(' - From : %s (%s)',$msg->from,$msg->fftn->ftn));
if ($msg instanceof Echomail)
$this->warn(sprintf(' - To : %s',$msg->to));
else
$this->warn(sprintf(' - To : %s (%s)',$msg->to,$msg->tftn->ftn));
$this->warn(sprintf(' - Subject: %s',$msg->subject));
if ($msg instanceof Echomail)
$this->warn(sprintf(' - Area : %s',$msg->echoarea->name));
if ($msg->errors->count()) {
echo "\n";
$this->error("Errors:");
foreach ($msg->errors->all() as $error)
$this->error(' - '.$error);
}
} catch (\Exception $e) { } catch (\Exception $e) {
$this->error('! ERROR: '.$e->getMessage()); $this->error('! ERROR: '.$e->getMessage());
@ -76,15 +99,17 @@ class PacketInfo extends Command
foreach ($pkt->errors as $msg) { foreach ($pkt->errors as $msg) {
$this->error(sprintf('- Date: %s',$msg->date)); $this->error(sprintf('- Date: %s',$msg->date));
$this->error(sprintf(' - FLAGS: %s',$msg->flags()->filter()->keys()->join(', '))); $this->error(sprintf(' - FLAGS: %s',$msg->flags()->filter()->keys()->join(', ')));
$this->error(sprintf(' - From: %s (%s)',$msg->user_from,$msg->fftn)); $this->error(sprintf(' - From: %s (%s)',$msg->from,$msg->fftn));
$this->error(sprintf(' - To: %s (%s)',$msg->user_to,$msg->tftn)); $this->error(sprintf(' - To: %s (%s)',$msg->to,$msg->tftn));
$this->error(sprintf(' - Subject: %s',$msg->subject)); $this->error(sprintf(' - Subject: %s',$msg->subject));
foreach ($msg->errors->errors()->all() as $error) foreach ($msg->errors->all() as $error)
$this->line(' - '.$error); $this->line(' - '.$error);
} }
$this->line("\n"); $this->line("\n");
} }
return self::SUCCESS;
} }
} }

View File

@ -2,14 +2,39 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Classes\File;
use App\Classes\FTN\Packet; use App\Classes\FTN\Packet;
use App\Jobs\MessageProcess as Job; use App\Jobs\PacketProcess as Job;
use App\Models\Address; use App\Models\Address;
/**
* Things to test
* + Packet
* - Sender doesnt exist (try and send a bounce message in the same session)
* - Sender defined in DB by not ours
* - Sender has wrong password
* - Packet too old
*
* + Echomail
* - Area doesnt exist (to uplink)
* - Sender not subscribed (to uplink)
* - Sender cannot post (to uplink)
* - Sender has wrong address for echorea domain (to uplink)
* - Test message in echoarea
* - Echomail from address doesnt match packet envelope (to uplink)
* - Echomail too old (to uplink)
* - Rescanned dont generate notifications
* - Rescanned dont trigger bots
* - Some Notifications to an uplink should go to the admin instead?
*
* + Netmail
* - To hub, and user not defined (reject)
* - To hub, but user redirect (redirected)
* - To areafix (processed)
* - To ping (respond)
* - With trace turned on (respond)
*/
class PacketProcess extends Command class PacketProcess extends Command
{ {
/** /**
@ -20,7 +45,8 @@ class PacketProcess extends Command
protected $signature = 'packet:process' protected $signature = 'packet:process'
.' {file : Packet to process}' .' {file : Packet to process}'
.' {--N|nobot : Dont process bots}' .' {--N|nobot : Dont process bots}'
.' {ftn : System the packet is from}'; .' {ftn? : System the packet is from}'
.' {--Q|dontqueue : Dont queue the message}';
/** /**
* The console command description. * The console command description.
@ -32,24 +58,28 @@ class PacketProcess extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
* @throws \App\Classes\FTN\InvalidPacketException * @throws \Exception
*/ */
public function handle() public function handle(): int
{ {
$f = new File($this->argument('file')); $rel_name = sprintf('%s/%s',config('fido.dir'),$this->argument('file'));
$a = Address::findFTN($this->argument('ftn'));
foreach ($f as $packet) { $m = [];
foreach (Packet::process($packet,$f->itemName(),$f->itemSize(),$a->system) as $msg) { if ($this->argument('ftn')) {
// @todo Quick check that the packet should be processed by us. $ao = Address::findFTN($this->argument('ftn'));
// @todo validate that the packet's zone is in the domain.
$this->info(sprintf('Processing message from [%s] with msgid [%s] in (%s)',$msg->fboss,$msg->msgid,$f->pktName())); } elseif (preg_match(sprintf('/^%s\.(.{3})$/',Packet::regex),$this->argument('file'),$m)) {
$ao = Address::findOrFail(hexdec($m[1]));
// Dispatch job. } else {
Job::dispatchSync($msg,$f->pktName(),$a,$a,Carbon::now(),$this->option('nobot')); $this->error('Unable to determine sender FTN address');
}
} return self::FAILURE;
}
Job::dispatchSync($rel_name,$ao->zone->domain,$this->option('dontqueue'));
return self::SUCCESS;
} }
} }

View File

@ -4,7 +4,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\System; use App\Models\Address;
class PacketSystem extends Command class PacketSystem extends Command
{ {
@ -14,7 +14,7 @@ class PacketSystem extends Command
* @var string * @var string
*/ */
protected $signature = 'packet:system' protected $signature = 'packet:system'
.' {sid : System ID}'; .' {ftn : System address}';
/** /**
* The console command description. * The console command description.
@ -26,21 +26,31 @@ class PacketSystem extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
* @throws \App\Classes\FTN\InvalidPacketException * @throws \Exception
*/ */
public function handle() public function handle(): int
{ {
$so = System::findOrFail($this->argument('sid')); $ao = Address::findFTN($this->argument('ftn'));
foreach ($so->addresses as $ao) { foreach ($ao->system->addresses->where('validated',TRUE) as $o) {
$pkt = $ao->getEchomail(FALSE); $pkt = $o->getEchomail();
$this->info(sprintf('System address [%s] has [%d] messages.',$ao->ftn,$pkt?->count())); $this->info(sprintf('System address [%s] has [%d] echomail messages.',$o->ftn,$pkt?->count()));
if ($pkt) { if ($pkt) {
foreach ($pkt as $msg) foreach ($pkt as $msg)
$this->warn(sprintf('- %s',$msg->msgid)); $this->warn(sprintf('- %s (%s)',$msg->msgid,$msg->id));
}
$pkt = $o->getNetmail();
$this->info(sprintf('System address [%s] has [%d] netmail messages.',$o->ftn,$pkt?->count()));
if ($pkt) {
foreach ($pkt as $msg)
$this->warn(sprintf('- %s (%s)',$msg->msgid,$msg->id));
} }
} }
return self::SUCCESS;
} }
} }

View File

@ -30,10 +30,10 @@ class ServerStart extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return void * @return int
* @throws SocketException * @throws SocketException
*/ */
public function handle() public function handle(): int
{ {
Log::info(sprintf('%s:+ Server Starting (%d)',self::LOGKEY,getmypid())); Log::info(sprintf('%s:+ Server Starting (%d)',self::LOGKEY,getmypid()));
$o = Setup::findOrFail(config('app.id')); $o = Setup::findOrFail(config('app.id'));
@ -71,7 +71,7 @@ class ServerStart extends Command
if (! $start->count()) { if (! $start->count()) {
Log::alert(sprintf('%s:! No servers configured to start',self::LOGKEY)); Log::alert(sprintf('%s:! No servers configured to start',self::LOGKEY));
return; return self::FAILURE;
} }
pcntl_signal(SIGCHLD,SIG_IGN); pcntl_signal(SIGCHLD,SIG_IGN);
@ -105,7 +105,7 @@ class ServerStart extends Command
Log::info(sprintf('%s:= Finished: [%s]',self::LOGKEY,$item)); Log::info(sprintf('%s:= Finished: [%s]',self::LOGKEY,$item));
// Child finished we need to get out of this loop. // Child finished we need to get out of this loop.
exit; return self::SUCCESS;
} }
Log::debug(sprintf('%s:- Forked for [%s] (%d)',self::LOGKEY,$item,$pid)); Log::debug(sprintf('%s:- Forked for [%s] (%d)',self::LOGKEY,$item,$pid));
@ -125,5 +125,7 @@ class ServerStart extends Command
// Done // Done
Log::debug(sprintf('%s:= Finished.',self::LOGKEY)); Log::debug(sprintf('%s:= Finished.',self::LOGKEY));
return self::SUCCESS;
} }
} }

View File

@ -0,0 +1,35 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Jobs\SystemHeartbeat as Job;
class SystemHeartbeat extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'system:heartbeat'
.' {--F|force : Force poll systems that are auto hold}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Poll systems that we havent seen for a while';
/**
* Execute the console command.
*/
public function handle(): int
{
Log::info('CSH:- Triggering heartbeat to systems');
return Job::dispatchSync($this->option('force'));
}
}

View File

@ -29,11 +29,9 @@ class TicProcess extends Command
* *
* @return int * @return int
*/ */
public function handle() public function handle(): int
{ {
// Dispatch job. // Dispatch job.
Job::dispatchSync($this->argument('file'),$this->argument('domain')); return Job::dispatchSync($this->argument('file'),$this->argument('domain'));
return Command::SUCCESS;
} }
} }

View File

@ -19,14 +19,16 @@ class UserCodeSend extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return mixed * @return int
* @throws \Exception * @throws \Exception
*/ */
public function handle() public function handle(): int
{ {
$ao = Address::findFTN($this->argument('ftn')); $ao = Address::findFTN($this->argument('ftn'));
$uo = User::where('email',$this->argument('email'))->singleOrFail(); $uo = User::where('email',$this->argument('email'))->singleOrFail();
Notification::route('netmail',$ao->parent())->notify(new AddressLink($uo)); Notification::route('netmail',$ao->uplink())->notify(new AddressLink($uo));
return self::SUCCESS;
} }
} }

View File

@ -27,12 +27,14 @@ class UserMakeAdmin extends Command
* *
* @return int * @return int
*/ */
public function handle() public function handle(): int
{ {
$o = User::where('email',$this->argument('email'))->firstOrfail(); $o = User::where('email',$this->argument('email'))->firstOrfail();
$o->admin = ! $o->admin; $o->admin = ! $o->admin;
$o->save(); $o->save();
$this->info(sprintf('User [%s] %s an admin',$o->email,$o->admin ? 'IS' : 'is NOT')); $this->info(sprintf('User [%s] %s an admin',$o->email,$o->admin ? 'IS' : 'is NOT'));
return self::SUCCESS;
} }
} }

View File

@ -2,10 +2,11 @@
namespace App\Console; namespace App\Console;
use App\Jobs\MailSend;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Jobs\{AddressIdleDomain,MailSend,SystemHeartbeat};
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
/** /**
@ -27,6 +28,8 @@ class Kernel extends ConsoleKernel
{ {
$schedule->job(new MailSend(TRUE))->everyMinute()->withoutOverlapping(); $schedule->job(new MailSend(TRUE))->everyMinute()->withoutOverlapping();
$schedule->job(new MailSend(FALSE))->twiceDaily(1,13); $schedule->job(new MailSend(FALSE))->twiceDaily(1,13);
$schedule->job(new SystemHeartbeat)->hourly();
$schedule->job(new AddressIdleDomain)->weeklyOn(0,'01:00');
} }
/** /**

View File

@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use Exception;
class InvalidCRCException extends Exception
{
}

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