View difference between Paste ID: AwGFFnYm and hi4xFVxn
SHOW: | | - or go back to the newest paste.
1
--
2
--  Firewolf Server
3
--  Made by 1lann and GravityScore
4
--
5
6
--  Networking API
7
	--  RC4
8
		--    RC4
9
		--    Implementation by AgentE382
10
11
12
		local cryptWrapper = function(plaintext, salt)
13
			local key = type(salt) == "table" and {unpack(salt)} or {string.byte(salt, 1, #salt)}
14
			local S = {}
15
			for i = 0, 255 do
16
				S[i] = i
17
			end
18
19
			local j, keylength = 0, #key
20
			for i = 0, 255 do
21
				j = (j + S[i] + key[i % keylength + 1]) % 256
22
				S[i], S[j] = S[j], S[i]
23
			end
24
25
			local i = 0
26
			j = 0
27
			local chars, astable = type(plaintext) == "table" and {unpack(plaintext)} or {string.byte(plaintext, 1, #plaintext)}, false
28
29
			for n = 1, #chars do
30
				i = (i + 1) % 256
31
				j = (j + S[i]) % 256
32
				S[i], S[j] = S[j], S[i]
33
				chars[n] = bit.bxor(S[(S[i] + S[j]) % 256], chars[n])
34
				if chars[n] > 127 or chars[n] == 13 then
35
					astable = true
36
				end
37
			end
38
39
			return astable and chars or string.char(unpack(chars))
40
		end
41
42
43
		local crypt = function(text, key)
44
			local resp, msg = pcall(cryptWrapper, text, key)
45
			if resp then
46
				return msg
47
			else
48
				return nil
49
			end
50
		end
51
52
53
	--  Base64
54
		--
55
		--  Base64 Encryption/Decryption
56
		--  By KillaVanilla
57
		--  http://www.computercraft.info/forums2/index.php?/topic/12450-killavanillas-various-apis/
58
		--  http://pastebin.com/rCYDnCxn
59
		--
60
61
62
63
		local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
64
65
66
		local function sixBitToBase64(input)
67
			return string.sub(alphabet, input+1, input+1)
68
		end
69
70
71
		local function base64ToSixBit(input)
72
			for i=1, 64 do
73
				if input == string.sub(alphabet, i, i) then
74
					return i-1
75
				end
76
			end
77
		end
78
79
80
		local function octetToBase64(o1, o2, o3)
81
			local shifted = bit.brshift(bit.band(o1, 0xFC), 2)
82
			local i1 = sixBitToBase64(shifted)
83
			local i2 = "A"
84
			local i3 = "="
85
			local i4 = "="
86
			if o2 then
87
				i2 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o1, 3), 4), bit.brshift(bit.band(o2, 0xF0), 4) ))
88
				if not o3 then
89
					i3 = sixBitToBase64(bit.blshift(bit.band(o2, 0x0F), 2))
90
				else
91
					i3 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o2, 0x0F), 2), bit.brshift(bit.band(o3, 0xC0), 6) ))
92
				end
93
			else
94
				i2 = sixBitToBase64(bit.blshift(bit.band(o1, 3), 4))
95
			end
96
			if o3 then
97
				i4 = sixBitToBase64(bit.band(o3, 0x3F))
98
			end
99
100
			return i1..i2..i3..i4
101
		end
102
103
104
		local function base64ToThreeOctet(s1)
105
			local c1 = base64ToSixBit(string.sub(s1, 1, 1))
106
			local c2 = base64ToSixBit(string.sub(s1, 2, 2))
107
			local c3 = 0
108
			local c4 = 0
109
			local o1 = 0
110
			local o2 = 0
111
			local o3 = 0
112
			if string.sub(s1, 3, 3) == "=" then
113
				c3 = nil
114
				c4 = nil
115
			elseif string.sub(s1, 4, 4) == "=" then
116
				c3 = base64ToSixBit(string.sub(s1, 3, 3))
117
				c4 = nil
118
			else
119
				c3 = base64ToSixBit(string.sub(s1, 3, 3))
120
				c4 = base64ToSixBit(string.sub(s1, 4, 4))
121
			end
122
			o1 = bit.bor( bit.blshift(c1, 2), bit.brshift(bit.band( c2, 0x30 ), 4) )
123
			if c3 then
124
				o2 = bit.bor( bit.blshift(bit.band(c2, 0x0F), 4), bit.brshift(bit.band( c3, 0x3C ), 2) )
125
			else
126
				o2 = nil
127
			end
128
			if c4 then
129
				o3 = bit.bor( bit.blshift(bit.band(c3, 3), 6), c4 )
130
			else
131
				o3 = nil
132
			end
133
			return o1, o2, o3
134
		end
135
136
137
		local function splitIntoBlocks(bytes)
138
			local blockNum = 1
139
			local blocks = {}
140
			for i=1, #bytes, 3 do
141
				blocks[blockNum] = {bytes[i], bytes[i+1], bytes[i+2]}
142
				blockNum = blockNum+1
143
			end
144
			return blocks
145
		end
146
147
148
		function base64Encode(bytes)
149
			local blocks = splitIntoBlocks(bytes)
150
			local output = ""
151
			for i=1, #blocks do
152
				output = output..octetToBase64( unpack(blocks[i]) )
153
			end
154
			return output
155
		end
156
157
158
		function base64Decode(str)
159
			local bytes = {}
160
			local blocks = {}
161
			local blockNum = 1
162
163
			for i=1, #str, 4 do
164
				blocks[blockNum] = string.sub(str, i, i+3)
165
				blockNum = blockNum+1
166
			end
167
168
			for i=1, #blocks do
169
				local o1, o2, o3 = base64ToThreeOctet(blocks[i])
170
				table.insert(bytes, o1)
171
				table.insert(bytes, o2)
172
				table.insert(bytes, o3)
173
			end
174
175
			return bytes
176
		end
177
178
179
	--  SHA-256
180
		--
181
		--  Adaptation of the Secure Hashing Algorithm (SHA-244/256)
182
		--  Found Here: http://lua-users.org/wiki/SecureHashAlgorithm
183
		--
184
		--  Using an adapted version of the bit library
185
		--  Found Here: https://bitbucket.org/Boolsheet/bslf/src/1ee664885805/bit.lua
186
		--
187
188
189
190
		local MOD = 2^32
191
		local MODM = MOD-1
192
193
194
		local function memoize(f)
195
			local mt = {}
196
			local t = setmetatable({}, mt)
197
			function mt:__index(k)
198
				local v = f(k)
199
				t[k] = v
200
				return v
201
			end
202
			return t
203
		end
204
205
206
		local function make_bitop_uncached(t, m)
207
			local function bitop(a, b)
208
				local res,p = 0,1
209
				while a ~= 0 and b ~= 0 do
210
					local am, bm = a % m, b % m
211
					res = res + t[am][bm] * p
212
					a = (a - am) / m
213
					b = (b - bm) / m
214
					p = p * m
215
				end
216
				res = res + (a + b) * p
217
				return res
218
			end
219
220
			return bitop
221
		end
222
223
224
		local function make_bitop(t)
225
			local op1 = make_bitop_uncached(t,2^1)
226
			local op2 = memoize(function(a)
227
				return memoize(function(b)
228
					return op1(a, b)
229
				end)
230
			end)
231
			return make_bitop_uncached(op2, 2 ^ (t.n or 1))
232
		end
233
234
235
		local customBxor1 = make_bitop({[0] = {[0] = 0,[1] = 1}, [1] = {[0] = 1, [1] = 0}, n = 4})
236
237
		local function customBxor(a, b, c, ...)
238
			local z = nil
239
			if b then
240
				a = a % MOD
241
				b = b % MOD
242
				z = customBxor1(a, b)
243
				if c then
244
					z = customBxor(z, c, ...)
245
				end
246
				return z
247
			elseif a then
248
				return a % MOD
249
			else
250
				return 0
251
			end
252
		end
253
254
255
		local function customBand(a, b, c, ...)
256
			local z
257
			if b then
258
				a = a % MOD
259
				b = b % MOD
260
				z = ((a + b) - customBxor1(a,b)) / 2
261
				if c then
262
					z = customBand(z, c, ...)
263
				end
264
				return z
265
			elseif a then
266
				return a % MOD
267
			else
268
				return MODM
269
			end
270
		end
271
272
273
		local function bnot(x)
274
			return (-1 - x) % MOD
275
		end
276
277
278
		local function rshift1(a, disp)
279
			if disp < 0 then
280
				return lshift(a, -disp)
281
			end
282
			return math.floor(a % 2 ^ 32 / 2 ^ disp)
283
		end
284
285
286
		local function rshift(x, disp)
287
			if disp > 31 or disp < -31 then
288
				return 0
289
			end
290
			return rshift1(x % MOD, disp)
291
		end
292
293
294
		local function lshift(a, disp)
295
			if disp < 0 then
296
				return rshift(a, -disp)
297
			end
298
			return (a * 2 ^ disp) % 2 ^ 32
299
		end
300
301
302
		local function rrotate(x, disp)
303
		    x = x % MOD
304
		    disp = disp % 32
305
		    local low = customBand(x, 2 ^ disp - 1)
306
		    return rshift(x, disp) + lshift(low, 32 - disp)
307
		end
308
309
310
		local k = {
311
			0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
312
			0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
313
			0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
314
			0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
315
			0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
316
			0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
317
			0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
318
			0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
319
			0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
320
			0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
321
			0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
322
			0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
323
			0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
324
			0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
325
			0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
326
			0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
327
		}
328
329
330
		local function str2hexa(s)
331
			return (string.gsub(s, ".", function(c)
332
				return string.format("%02x", string.byte(c))
333
			end))
334
		end
335
336
337
		local function num2s(l, n)
338
			local s = ""
339
			for i = 1, n do
340
				local rem = l % 256
341
				s = string.char(rem) .. s
342
				l = (l - rem) / 256
343
			end
344
			return s
345
		end
346
347
348
		local function s232num(s, i)
349
			local n = 0
350
			for i = i, i + 3 do
351
				n = n*256 + string.byte(s, i)
352
			end
353
			return n
354
		end
355
356
357
		local function preproc(msg, len)
358
			local extra = 64 - ((len + 9) % 64)
359
			len = num2s(8 * len, 8)
360
			msg = msg .. "\128" .. string.rep("\0", extra) .. len
361
			assert(#msg % 64 == 0)
362
			return msg
363
		end
364
365
366
		local function initH256(H)
367
			H[1] = 0x6a09e667
368
			H[2] = 0xbb67ae85
369
			H[3] = 0x3c6ef372
370
			H[4] = 0xa54ff53a
371
			H[5] = 0x510e527f
372
			H[6] = 0x9b05688c
373
			H[7] = 0x1f83d9ab
374
			H[8] = 0x5be0cd19
375
			return H
376
		end
377
378
379
		local function digestblock(msg, i, H)
380
			local w = {}
381
			for j = 1, 16 do
382
				w[j] = s232num(msg, i + (j - 1)*4)
383
			end
384
			for j = 17, 64 do
385
				local v = w[j - 15]
386
				local s0 = customBxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3))
387
				v = w[j - 2]
388
				w[j] = w[j - 16] + s0 + w[j - 7] + customBxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10))
389
			end
390
391
			local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8]
392
			for i = 1, 64 do
393
				local s0 = customBxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22))
394
				local maj = customBxor(customBand(a, b), customBand(a, c), customBand(b, c))
395
				local t2 = s0 + maj
396
				local s1 = customBxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25))
397
				local ch = customBxor (customBand(e, f), customBand(bnot(e), g))
398
				local t1 = h + s1 + ch + k[i] + w[i]
399
				h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2
400
			end
401
402
			H[1] = customBand(H[1] + a)
403
			H[2] = customBand(H[2] + b)
404
			H[3] = customBand(H[3] + c)
405
			H[4] = customBand(H[4] + d)
406
			H[5] = customBand(H[5] + e)
407
			H[6] = customBand(H[6] + f)
408
			H[7] = customBand(H[7] + g)
409
			H[8] = customBand(H[8] + h)
410
		end
411
412
413
		local function sha256(msg)
414
			msg = preproc(msg, #msg)
415
			local H = initH256({})
416
			for i = 1, #msg, 64 do
417
				digestblock(msg, i, H)
418
			end
419
			return str2hexa(num2s(H[1], 4) .. num2s(H[2], 4) .. num2s(H[3], 4) .. num2s(H[4], 4) ..
420
				num2s(H[5], 4) .. num2s(H[6], 4) .. num2s(H[7], 4) .. num2s(H[8], 4))
421
		end
422
423
424
	local protocolName = "Firewolf"
425
426
427
	--  Cryptography
428
		local Cryptography = {}
429
		Cryptography.sha = {}
430
		Cryptography.base64 = {}
431
		Cryptography.aes = {}
432
433
434
		function Cryptography.bytesFromMessage(msg)
435
			local bytes = {}
436
437
			for i = 1, msg:len() do
438
				local letter = string.byte(msg:sub(i, i))
439
				table.insert(bytes, letter)
440
			end
441
442
			return bytes
443
		end
444
445
446
		function Cryptography.messageFromBytes(bytes)
447
			local msg = ""
448
449
			for i = 1, #bytes do
450
				local letter = string.char(bytes[i])
451
				msg = msg .. letter
452
			end
453
454
			return msg
455
		end
456
457
458
		function Cryptography.bytesFromKey(key)
459
			local bytes = {}
460
461
			for i = 1, key:len() / 2 do
462
				local group = key:sub((i - 1) * 2 + 1, (i - 1) * 2 + 1)
463
				local num = tonumber(group, 16)
464
				table.insert(bytes, num)
465
			end
466
467
			return bytes
468
		end
469
470
471
		function Cryptography.sha.sha256(msg)
472
			return sha256(msg)
473
		end
474
475
476
		function Cryptography.aes.encrypt(msg, key)
477
			return base64Encode(crypt(msg, key))
478
		end
479
480
481
		function Cryptography.aes.decrypt(msg, key)
482
			return crypt(base64Decode(msg), key)
483
		end
484
485
486
		function Cryptography.base64.encode(msg)
487
			return base64Encode(Cryptography.bytesFromMessage(msg))
488
		end
489
490
491
		function Cryptography.base64.decode(msg)
492
			return Cryptography.messageFromBytes(base64Decode(msg))
493
		end
494
495
		function Cryptography.channel(text)
496
			local hashed = Cryptography.sha.sha256(text)
497
498
			local total = 0
499
500
			for i = 1, hashed:len() do
501
				total = total + string.byte(hashed:sub(i, i))
502
			end
503
504
			return (total % 55530) + 10000
505
		end
506
507
		function Cryptography.sanatize(text)
508
			local sanatizeChars = {"%", "(", ")", "[", "]", ".", "+", "-", "*", "?", "^", "$"}
509
510
			for _, char in pairs(sanatizeChars) do
511
				text = text:gsub("%"..char, "%%%"..char)
512
			end
513
			return text
514
		end
515
516
517
	--  Modem
518
		local Modem = {}
519
520
		Modem.modems = {}
521
522
		function Modem.exists()
523
			Modem.exists = false
524
			for _, side in pairs(rs.getSides()) do
525
				if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then
526
					Modem.exists = true
527
528
					if not Modem.modems[side] then
529
						Modem.modems[side] = peripheral.wrap(side)
530
					end
531
				end
532
			end
533
534
			return Modem.exists
535
		end
536
537
538
		function Modem.open(channel)
539
			if not Modem.exists then
540
				return false
541
			end
542
543
			for side, modem in pairs(Modem.modems) do
544
				modem.open(channel)
545
				rednet.open(side)
546
			end
547
548
			return true
549
		end
550
551
552
		function Modem.close(channel)
553
			if not Modem.exists then
554
				return false
555
			end
556
557
			for side, modem in pairs(Modem.modems) do
558
				modem.close(channel)
559
			end
560
561
			return true
562
		end
563
564
565
		function Modem.closeAll()
566
				if not Modem.exists then
567
					return false
568
				end
569
570
				for side, modem in pairs(Modem.modems) do
571
					modem.closeAll()
572
				end
573
574
				return true
575
		end
576
577
578
		function Modem.isOpen(channel)
579
			if not Modem.exists then
580
				return false
581
			end
582
583
			local isOpen = false
584
			for side, modem in pairs(Modem.modems) do
585
				if modem.isOpen(channel) then
586
					isOpen = true
587
					break
588
				end
589
			end
590
591
			return isOpen
592
		end
593
594
595
		function Modem.transmit(channel, msg)
596
			if not Modem.exists then
597
				return false
598
			end
599
600
			if not Modem.isOpen(channel) then
601
				Modem.open(channel)
602
			end
603
604
			for side, modem in pairs(Modem.modems) do
605
				modem.transmit(channel, channel, msg)
606
			end
607
608
			return true
609
		end
610
611
612
	--  Handshake
613
		local Handshake = {}
614
615
		Handshake.prime = 625210769
616
		Handshake.channel = 54569
617
		Handshake.base = -1
618
		Handshake.secret = -1
619
		Handshake.sharedSecret = -1
620
		Handshake.packetHeader = "["..protocolName.."-Handshake-Packet-Header]"
621
		Handshake.packetMatch = "%["..protocolName.."%-Handshake%-Packet%-Header%](.+)"
622
623
		function Handshake.exponentWithModulo(base, exponent, modulo)
624
			local remainder = base
625
626
			for i = 1, exponent-1 do
627
				remainder = remainder * remainder
628
				if remainder >= modulo then
629
					remainder = remainder % modulo
630
				end
631
			end
632
633
			return remainder
634
		end
635
636
637
		function Handshake.clear()
638
			Handshake.base = -1
639
			Handshake.secret = -1
640
			Handshake.sharedSecret = -1
641
		end
642
643
		function Handshake.generateInitiatorData()
644
			Handshake.base = math.random(10,99999)
645
			Handshake.secret = math.random(10,99999)
646
			return {
647
				type = "initiate",
648
				prime = Handshake.prime,
649
				base = Handshake.base,
650
				moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime)
651
			}
652
		end
653
654
		function Handshake.generateResponseData(initiatorData)
655
			local isPrimeANumber = type(initiatorData.prime) == "number"
656
			local isPrimeMatching = initiatorData.prime == Handshake.prime
657
			local isBaseANumber = type(initiatorData.base) == "number"
658
			local isInitiator = initiatorData.type == "initiate"
659
			local isModdedSecretANumber = type(initiatorData.moddedSecret) == "number"
660
			local areAllNumbersNumbers = isPrimeANumber and isBaseANumber and isModdedSecretANumber
661
662
			if areAllNumbersNumbers and isPrimeMatching then
663
				if isInitiator then
664
					Handshake.base = initiatorData.base
665
					Handshake.secret = math.random(10,99999)
666
					Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime)
667
					return {
668
						type = "response",
669
						prime = Handshake.prime,
670
						base = Handshake.base,
671
						moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime)
672
					}, Handshake.sharedSecret
673
				elseif initiatorData.type == "response" and Handshake.base > 0 and Handshake.secret > 0 then
674
					Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime)
675
					return Handshake.sharedSecret
676
				else
677
					return false
678
				end
679
			else
680
				return false
681
			end
682
		end
683
684
	--  Secure Connection
685
		local SecureConnection = {}
686
		SecureConnection.__index = SecureConnection
687
688
689
		SecureConnection.packetHeaderA = "["..protocolName.."-"
690
		SecureConnection.packetHeaderB = "-SecureConnection-Packet-Header]"
691
		SecureConnection.packetMatchA = "%["..protocolName.."%-"
692
		SecureConnection.packetMatchB = "%-SecureConnection%-Packet%-Header%](.+)"
693
		SecureConnection.connectionTimeout = 0.1
694
		SecureConnection.successPacketTimeout = 0.1
695
696
697
		function SecureConnection.new(secret, key, identifier, distance, isRednet)
698
			local self = setmetatable({}, SecureConnection)
699
			self:setup(secret, key, identifier, distance, isRednet)
700
			return self
701
		end
702
703
704
		function SecureConnection:setup(secret, key, identifier, distance, isRednet)
705
			local rawSecret
706
707
			if isRednet then
708
				self.isRednet = true
709
				self.distance = -1
710
				self.rednet_id = distance
711
				rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) ..
712
				"|" .. tostring(key) .. "|rednet"
713
			else
714
				self.isRednet = false
715
				self.distance = distance
716
				rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) ..
717
				"|" .. tostring(key) .. "|" .. tostring(distance)
718
			end
719
720
			self.identifier = identifier
721
			self.packetMatch = SecureConnection.packetMatchA .. Cryptography.sanatize(identifier) .. SecureConnection.packetMatchB
722
			self.packetHeader = SecureConnection.packetHeaderA .. identifier .. SecureConnection.packetHeaderB
723
			self.secret = Cryptography.sha.sha256(rawSecret)
724
			self.channel = Cryptography.channel(self.secret)
725
726
			if not self.isRednet then
727
				Modem.open(self.channel)
728
			end
729
		end
730
731
732
		function SecureConnection:verifyHeader(msg)
733
			if type(msg) ~= "string" then return false end
734
		
735
			if msg:match(self.packetMatch) then
736
				return true
737
			else
738
				return false
739
			end
740
		end
741
742
743
		function SecureConnection:sendMessage(msg, rednetProtocol)
744
			local rawEncryptedMsg = Cryptography.aes.encrypt(self.packetHeader .. msg, self.secret)
745
			local encryptedMsg = self.packetHeader .. rawEncryptedMsg
746
747
			if self.isRednet then
748
				rednet.send(self.rednet_id, encryptedMsg, rednetProtocol)
749
				return true
750
			else
751
				return Modem.transmit(self.channel, encryptedMsg)
752
			end
753
		end
754
755
756
		function SecureConnection:decryptMessage(msg)
757
			if self:verifyHeader(msg) then
758
				local encrypted = msg:match(self.packetMatch)
759
760
				local unencryptedMsg = nil
761
				pcall(function() unencryptedMsg = Cryptography.aes.decrypt(encrypted, self.secret) end)
762
				if not unencryptedMsg then
763
					return false, "Could not decrypt"
764
				end
765
766
				if self:verifyHeader(unencryptedMsg) then
767
					return true, unencryptedMsg:match(self.packetMatch)
768
				else
769
					return false, "Could not verify"
770
				end
771
			else
772
				return false, "Could not stage 1 verify"
773
			end
774
		end
775
-- END OF NETWORKING
776
777
dnsListenChannel = 9999
778
dnsResponseChannel = 9998
779
780
local config = {}
781
config.visibleLoggingLevel = 1
782
config.writtenLoggingLevel = 1
783
config.loggingLocation = "/fwserver-logs"
784
config.enableLogging = false
785
config.password = null
786
config.allowRednetConnections = true
787
config.repeatRednetMessages = false
788
config.lastDomain = nil
789
--config.actAsGPS = false
790
--config.gpsLocation = {}
791
792
local configLocation = "/.fwserver-config"
793
local serverLocation = "/fw_servers"
794
local serverAPILocation = "server_api"
795
local locked = false
796
local domain = ...
797
local responseStack = {}
798
local repeatStack = {}
799
local terminalLog = {}
800
local repeatedMessages = {}
801
local lastLogNum = 0
802
local servedRequests = 0
803
local serverChannel = 0
804
local requestMatch = ""
805
local maliciousMatch = ""
806
local responseHeader = ""
807
local pageRequestMatch = ""
808
local pageResposneHeader = ""
809
local renableRednet = nil
810
local handles = {}
811
local customCoroutines = {}
812
local globalHandler = nil
813
local closeMatch = ""
814
local connections = {}
815
local updateURL = "https://raw.githubusercontent.com/1lann/Firewolf/master/src/server.lua"
816
817
local version = "3.5.2"
818
819
local header = {}
820
header.dnsPacket = "[Firewolf-DNS-Packet]"
821
header.dnsHeader = "[Firewolf-DNS-Response]"
822
header.dnsHeaderMatch = "^%[Firewolf%-DNS%-Response%](.+)$"
823
header.rednetHeader = "[Firewolf-Rednet-Channel-Simulation]"
824
header.rednetMatch = "^%[Firewolf%-Rednet%-Channel%-Simulation%](%d+)$"
825
header.requestMatchA = "^%[Firewolf%-"
826
header.requestMatchB = "%-Handshake%-Request%](.+)$"
827
header.maliciousMatchA = "^%[Firewolf%-"
828
header.maliciousMatchB = "%-.+%-Handshake%-Response%](.+)$"
829
header.responseHeaderA = "[Firewolf-"
830
header.responseHeaderB = "-"
831
header.responseHeaderC = "-Handshake-Response]"
832
header.pageRequestMatchA = "^%[Firewolf%-"
833
header.pageRequestMatchB = "%-Page%-Request%](.+)$"
834
header.pageResponseHeaderA = "[Firewolf-"
835
header.pageResponseHeaderB = "-Page-Response][HEADER]"
836
header.pageResponseHeaderC = "[BODY]"
837
header.closeMatchA = "[Firewolf-"
838
header.closeMatchB = "-Connection-Close]"
839
840
local resetServerEvent = "firewolf-server-reset-event"
841
842
local theme = {}
843
theme.error = colors.red
844
theme.background = colors.gray
845
theme.text = colors.white
846
theme.notice = colors.white
847
theme.barColor = colors.red
848
theme.barText = colors.white
849
theme.inputColor = colors.white
850
theme.clock = colors.white
851
theme.lock = colors.orange
852
theme.userResponse = colors.orange
853
854
if not term.isColor() then
855
	theme.error = colors.white
856
	theme.background = colors.black
857
	theme.text = colors.white
858
	theme.notice = colors.white
859
	theme.barColor = colors.white
860
	theme.barText = colors.black
861
	theme.inputColor = colors.white
862
	theme.clock = colors.white
863
	theme.lock = colors.white
864
	theme.userResponse = colors.white
865
end
866
867
local w, h = term.getSize()
868
869
--  Utilities
870
871
local makeDirectory = function(path)
872
	fs.makeDir(path)
873
	local function createIndex(path)
874
		if not (fs.exists(path.."/index") or fs.exists(path.."/index.fwml"))  then
875
			f = io.open(path.."/index", "w")
876
			f:write("print('')\ncenter('Welcome to "..domain.."!')")
877
			f:close()
878
		end
879
	end
880
	pcall(function() createIndex(path) end)
881
end
882
883
local checkDomain = function(domain)
884
	if domain:find("/") or domain:find(":") or domain:find("%?") then
885
		return "symbols"
886
	end
887
	if #domain < 4 then
888
		return "short"
889
	else
890
		Modem.open(dnsListenChannel)
891
		Modem.open(dnsResponseChannel)
892
		Modem.transmit(dnsListenChannel, header.dnsPacket)
893
		Modem.close(dnsListenChannel)
894
895
		rednet.broadcast(header.dnsPacket, header.rednetHeader .. dnsListenChannel)
896
		local timer = os.startTimer(2)
897
		while true do
898
			local event, id, channel, protocol, message, dist = os.pullEventRaw()
899
			if event == "modem_message" and channel == dnsResponseChannel and type(message) == string and message:match(header.dnsHeaderMatch) == domain then
900
				return "taken"
901
			elseif event == "rednet_message" and protocol and tonumber(protocol:match(header.rednetMatch)) == dnsResponseChannel and channel:match(header.dnsHeaderMatch) == domain then
902
				return "taken"
903
			elseif event == "timer" and id == timer then
904
				break
905
			end
906
		end
907
	end
908
	return "ok"
909
end
910
911
local checkConfig = function()
912
	local errorReport = {}
913
	if not config then
914
		table.insert(errorReport, "Corrupted configuration file!")
915
		return errorReport
916
	end
917
	if config.enableLogging then
918
		if fs.isReadOnly(config.loggingLocation) or fs.isDir(config.loggingLocation) then
919
			table.insert(errorReport, "Invalid logging location!")
920
		end
921
	end
922
923
	if #errorReport > 0 then
924
		return errorReport
925
	else
926
		return false
927
	end
928
end
929
930
local loadConfig = function()
931
	if fs.exists(configLocation) and not fs.isDir(configLocation) then
932
		local f = io.open(configLocation, "r")
933
		config = textutils.unserialize(f:read("*a"))
934
		if config then
935
			if type(config.actAsRednetRepeater) == "boolean" then
936
				config.actAsRednetRepeater = nil
937
			end
938
		end
939
		f:close()
940
	else
941
		config = nil
942
	end
943
end
944
945
local saveConfig = function()
946
	local f = io.open(configLocation, "w")
947
	f:write(textutils.serialize(config))
948
	f:close()
949
end
950
951
local center = function(text)
952
	local x, y = term.getCursorPos()
953
	term.setCursorPos(math.floor(w / 2 - text:len() / 2) + (text:len() % 2 == 0 and 1 or 0), y)
954
	term.write(text)
955
	term.setCursorPos(1, y + 1)
956
end
957
958
local writeLog = function(text, color, level)
959
	if not level then level = 0 end
960
	if not color then color = theme.text end
961
	if level >= config.visibleLoggingLevel then
962
		local time = textutils.formatTime(os.time(), true)
963
		if #time <= 4 then
964
			time = "0"..time
965
		end
966
		table.insert(terminalLog, {text, color, "["..time.."] "})
967
	end
968
969
	if config.enableLogging and level >= config.writtenLoggingLevel then
970
		if fs.isDir(config.loggingLocation) then
971
			fs.delete(config.loggingLocation)
972
		end
973
		if not fs.exists(config.loggingLocation) then
974
			local f = io.open(config.loggingLocation, "w")
975
			f:write("\n")
976
			f:close()
977
		end
978
		local time = textutils.formatTime(os.time(), true)
979
		if #time <= 4 then
980
			time = "0"..time
981
		end
982
		local f = io.open(config.loggingLocation, "a")
983
		f:write("["..time.."] "..text.."\n")
984
		f:close()
985
	end
986
end
987
988
local writeError = function(text)
989
	for i = 1, math.ceil(#text / (w - 8)) do
990
		writeLog(text:sub((i - 1) * (w - 8), i * (w - 8)), theme.error, math.huge)
991
	end
992
end
993
994
--  Message handling
995
996
local receiveDaemon = function()
997
	while true do
998
		local event, id, channel, protocol, message, dist = os.pullEventRaw()
999
		if event == "modem_message" then
1000
			if channel == rednet.CHANNEL_REPEAT and config.repeatRednetMessages and type(message) == "table" and
1001
				message.sProtocol and message.sProtocol:match(header.rednetMatch) then
1002
				table.insert(repeatStack, {message = message, reply = protocol})
1003
			elseif channel ~= rednet.CHANNEL_BROADCAST then
1004
				table.insert(responseStack, {type = "direct", channel = channel, message = message, dist = dist, reply = protocol})
1005
			end
1006
		elseif event == "rednet_message" and protocol and protocol:match(header.rednetMatch) and config.allowRednetConnections then
1007
			table.insert(responseStack, {type = "rednet", channel = tonumber(protocol:match(header.rednetMatch)), rednet_id = id, message = channel, dist = null})
1008
		elseif event == "timer" and id == renableRednet then
1009
			Modem.open(rednet.CHANNEL_REPEAT)
1010
		end
1011
	end
1012
end
1013
1014
local getActiveConnection = function(channel, distance)
1015
	for k, connection in pairs(connections) do
1016
		if connection.channel == channel then
1017
			if (not distance) then
1018
				if connection.isRednet then
1019
					return connection
1020
				else
1021
					return false
1022
				end
1023
			else
1024
				if connection.distance == distance then
1025
					return connection
1026
				else
1027
					return false
1028
				end
1029
			end
1030
		end
1031
	end
1032
end
1033
1034
local urlEncode = function(url)
1035
	local result = url
1036
1037
	result = result:gsub("%%", "%%a")
1038
	result = result:gsub(":", "%%c")
1039
	result = result:gsub("/", "%%s")
1040
	result = result:gsub("\n", "%%n")
1041
	result = result:gsub(" ", "%%w")
1042
	result = result:gsub("&", "%%m")
1043
	result = result:gsub("%?", "%%q")
1044
	result = result:gsub("=", "%%e")
1045
	result = result:gsub("%.", "%%d")
1046
1047
	return result
1048
end
1049
1050
local urlDecode = function(url)
1051
	local result = url
1052
1053
	result = result:gsub("%%c", ":")
1054
	result = result:gsub("%%s", "/")
1055
	result = result:gsub("%%n", "\n")
1056
	result = result:gsub("%%w", " ")
1057
	result = result:gsub("%%&", "&")
1058
	result = result:gsub("%%q", "%?")
1059
	result = result:gsub("%%e", "=")
1060
	result = result:gsub("%%d", "%.")
1061
	result = result:gsub("%%m", "%%")
1062
1063
	return result
1064
end
1065
1066
local getURLVars = function(url)
1067
	local vars = {}
1068
	if url then
1069
		local firstVarIndex, firstVarVal = url:match("%?([^=]+)=([^&]+)")
1070
		if not firstVarIndex then
1071
			return
1072
		else
1073
			vars[urlDecode(firstVarIndex)] = urlDecode(firstVarVal)
1074
			for index, val in url:gmatch("&([^=]+)=([^&]+)") do
1075
				vars[urlDecode(index)] = urlDecode(val)
1076
			end
1077
			return vars
1078
		end
1079
	else
1080
		return
1081
	end
1082
end
1083
1084
local fetchPage = function(page)
1085
	local pageRequest = fs.combine("", page:match("^[^%?]+"))
1086
	local varRequest = page:match("%?[^%?]+$")
1087
1088
	if (pageRequest:match("(.+)%.fwml$")) then
1089
		pageRequest = pageRequest:match("(.+)%.fwml$")
1090
	end
1091
1092
	pageRequest = pageRequest:gsub("%.%.", "")
1093
1094
	local handleResponse, respHeader
1095
1096
	if globalHandler then
1097
		err, msg = pcall(function() handleResponse, respHeader = globalHandler(page, getURLVars(varRequest)) end)
1098
		if not err then
1099
			writeLog("Error when executing server API function", theme.error, math.huge)
1100
			writeError("/: " .. tostring(msg))
1101
		end
1102
	end
1103
1104
	if handleResponse then
1105
		if not respHeader then
1106
			respHeader = "lua"
1107
		end
1108
		return handleResponse, respHeader, true
1109
	end
1110
1111
	if pageRequest == serverAPILocation then
1112
		-- Forbid accessing server api files
1113
		return nil
1114
	end
1115
1116
	for k,v in pairs(handles) do
1117
		local startSearch, endSearch = pageRequest:find(k)
1118
		if startSearch == 1 and ((endSearch == #pageRequest) or (pageRequest:sub(endSearch + 1, endSearch + 1) == "/")) then
1119
			err, msg = pcall(function() handleResponse, respHeader = v(page, getURLVars(varRequest)) end)
1120
			if not err then
1121
				writeLog("Error when executing server API function", theme.error, math.huge)
1122
				writeError(k .. ": " .. tostring(msg))
1123
			end
1124
		end
1125
	end
1126
1127
	if handleResponse then
1128
		if not respHeader then
1129
			respHeader = "lua"
1130
		end
1131
		return handleResponse, respHeader, true
1132
	end
1133
1134-
	local path = serverLocation .. "/" .. domain .. "/" .. pageRequest
1134+
	local path = serverLocation .. "/" .. pageRequest
1135
	if fs.exists(path) and not fs.isDir(path) then
1136
		local f = io.open(path, "r")
1137
		local contents = f:read("*a")
1138
		f:close()
1139
1140
		return contents, "lua", false
1141
	else
1142
		if fs.exists(path..".fwml") and not fs.isDir(path..".fwml") then
1143
			local f = io.open(path..".fwml", "r")
1144
			local contents = f:read("*a")
1145
			f:close()
1146
1147
			return contents, "fwml", false
1148
		end
1149
	end
1150
1151
	return nil
1152
end
1153
1154
local sendPage = function(connection, body, head)
1155
	if head == "fwml" then
1156
		connection:sendMessage(pageResposneHeader..'{["language"]="Firewolf Markup"}'..header.pageResponseHeaderC..body, header.rednetHeader .. connection.channel)
1157
	else
1158
		connection:sendMessage(pageResposneHeader..'{["language"]="Lua"}'..header.pageResponseHeaderC..body, header.rednetHeader .. connection.channel)
1159
	end
1160
end
1161
1162
local defaultNotFound = [[
1163
[br]
1164
[br]
1165
[=]
1166
The page you requested could not be found!
1167
[br]
1168
Make sure you typed the URL correctly.
1169
]]
1170
1171
local handlePageRequest = function(handler, index)
1172
	local connection = getActiveConnection(handler.channel, handler.dist)
1173
	if connection:verifyHeader(handler.message) then
1174
		local resp, data = connection:decryptMessage(handler.message)
1175
		if not resp then
1176
			-- Decryption Error
1177
			writeLog("Decryption error!", theme.notice, 0)
1178
		else
1179
			-- Process Request
1180
			if data:match(pageRequestMatch) then
1181
				local page = data:match(pageRequestMatch)
1182
				if page == "" or page == "/" then
1183
					page = "index"
1184
				end
1185
				local body, head, isAPI = fetchPage(page)
1186
				if body then
1187
					sendPage(connection, tostring(body), head)
1188
					if isAPI then
1189
						writeLog("API request: "..page:sub(1,25), theme.text, 0)
1190
					else
1191
						writeLog("Successful request: "..page:sub(1,20), theme.text, 1)
1192
					end
1193
				else
1194
					body, head, isAPI = fetchPage("not-found")
1195
					if body then
1196
						sendPage(connection, body, head)
1197
					else
1198
						sendPage(connection, defaultNotFound, "fwml")
1199
					end
1200
					writeLog("Unsuccessful request: "..page:sub(1,20), theme.text, 1)
1201
				end
1202
			elseif data == closeMatch then
1203
				writeLog("Secure connection closed", theme.text, 0)
1204
				Modem.close(connection.channel)
1205
				table.remove(connections, index)
1206
			end
1207
		end
1208
	end
1209
end
1210
1211
local handleHandshakeRequest = function(handler)
1212
	local requestData = handler.message:match(requestMatch)
1213
	if requestData and type(textutils.unserialize(requestData)) == "table" then
1214
		local receivedHandshake = textutils.unserialize(requestData)
1215
		local data, key = Handshake.generateResponseData(receivedHandshake)
1216
		if type(data) == "table" then
1217
			local connection
1218
1219
			if handler.type == "direct" then
1220
				connection = SecureConnection.new(key, domain, domain, handler.dist)
1221
1222
				Modem.transmit(serverChannel, responseHeader .. tostring(handler.dist) .. header.responseHeaderC .. textutils.serialize(data))
1223
			else
1224
				connection = SecureConnection.new(key, domain, domain, handler.rednet_id, true)
1225
1226
				rednet.send(handler.rednet_id, responseHeader .. tostring(handler.rednet_id) .. header.responseHeaderC .. textutils.serialize(data), header.rednetHeader .. serverChannel)
1227
			end
1228
1229
			writeLog("Secure connection opened", theme.text, 0)
1230
1231
			table.insert(connections, connection)
1232
1233
			if #connections >= 200 then
1234
				Modem.close(connections[1].channel)
1235
				table.remove(connections, 1)
1236
			end
1237
		end
1238
	elseif handler.message:match(maliciousMatch) then
1239
		-- Hijacking Detected
1240
		writeLog("Warning: Connection Hijacking Detected", theme.error, 2)
1241
	end
1242
end
1243
1244
local responseDaemon = function()
1245
	while true do
1246
		os.pullEventRaw()
1247
1248
		for k, v in pairs(responseStack) do
1249
			if v.channel then
1250
				if v.channel == dnsListenChannel and v.message == header.dnsPacket then
1251
					-- DNS Request
1252
					if v.type == "rednet" then
1253
						rednet.send(v.rednet_id, header.dnsHeader .. domain, header.rednetHeader .. dnsResponseChannel)
1254
					else
1255
						Modem.open(dnsResponseChannel)
1256
						Modem.transmit(dnsResponseChannel, header.dnsHeader .. domain)
1257
						Modem.close(dnsResponseChannel)
1258
					end
1259
				elseif v.channel == serverChannel and v.message then
1260
					handleHandshakeRequest(v)
1261
				elseif getActiveConnection(v.channel, v.dist) then
1262
					handlePageRequest(v, k)
1263
				end
1264
			end
1265
		end
1266
1267
		responseStack = {}
1268
1269
		if #repeatStack > 10 then
1270
			Modem.close(rednet.CHANNEL_REPEAT)
1271
			renableRednet = os.startTimer(2)
1272
			repeatStack = {}
1273
			writeLog("Thorttling Rednet Connections", theme.notice, 2)
1274
		end
1275
1276
		for k, v in pairs(repeatStack) do
1277
			if v.message.nMessageID and v.message.nRecipient then
1278
				if (not repeatedMessages[v.message.nMessageID]) or (os.clock() - repeatedMessages[v.message.nMessageID]) > 10 then
1279
					repeatedMessages[v.message.nMessageID] = os.clock()
1280
					for side, modem in pairs(Modem.modems) do
1281
						modem.transmit(rednet.CHANNEL_REPEAT, v.reply, v.message)
1282
						modem.transmit(v.message.nRecipient, v.reply, v.message)
1283
					end
1284
				end
1285
			end
1286
		end
1287
1288
		repeatStack = {}
1289
	end
1290
end
1291
1292
--  Commands and Help
1293
1294
local commands = {}
1295
local helpDocs = {}
1296
1297
commands["password"] = function(newPassword)
1298
	if not newPassword or newPassword == "" then
1299
		writeLog("Usage: password <new-password>", theme.userResponse, math.huge)
1300
	else
1301
		config.password = newPassword
1302
		saveConfig()
1303
		writeLog("New password set!", theme.userResponse, math.huge)
1304
	end
1305
end
1306
1307
commands["lock"] = function()
1308
	if config.password then
1309
		locked = true
1310
		writeLog("Server locked", theme.userResponse, math.huge)
1311
		os.queueEvent("firewolf-lock-state-update")
1312
	else
1313
		writeLog("No password has been set! Set a", theme.userResponse, math.huge)
1314
		writeLog("password with: password <password>", theme.userResponse, math.huge)
1315
	end
1316
end
1317
1318
commands["exit"] = function()
1319
	error("firewolf-exit")
1320
end
1321
1322
commands["stop"] = commands["exit"]
1323
1324
commands["quit"] = commands["exit"]
1325
1326
commands["startup"] = function()
1327
	if fs.exists("/startup") then
1328
		if fs.exists("/old-startup") then
1329
			fs.delete("/old-startup")
1330
		end
1331
		fs.move("/startup", "/old-startup")
1332
	end
1333
	local f = io.open("/startup", "w")
1334
	f:write([[
1335
	sleep(0.1)
1336
	shell.run("]]..shell.getRunningProgram().." "..domain.."\")")
1337
	f:close()
1338
	writeLog("Server will now run on startup!", theme.userResponse, math.huge)
1339
end
1340
1341
commands["rednet"] = function(set)
1342
	if set == "on" then
1343
		config.allowRednetConnections = true
1344
		saveConfig()
1345
		writeLog("Now allowing rednet connections", theme.userResponse, math.huge)
1346
	elseif set == "off" then
1347
		config.allowRednetConnections = false
1348
		saveConfig()
1349
		writeLog("Now dis-allowing rednet connections", theme.userResponse, math.huge)
1350
	else
1351
		if config.allowRednetConnections then
1352
			writeLog("Rednet conn. are currently allowed", theme.userResponse, math.huge)
1353
		else
1354
			writeLog("Rednet conn. are currently dis-allowed", theme.userResponse, math.huge)
1355
		end
1356
	end
1357
end
1358
1359
commands["restart"] = function()
1360
	error("firewolf-restart")
1361
end
1362
1363
commands["reboot"] = commands["restart"]
1364
1365
commands["reload"] = function()
1366
	os.queueEvent(resetServerEvent)
1367
end
1368
1369
commands["refresh"] = commands["reload"]
1370
1371
commands["repeat"] = function(set)
1372
	if set == "on" then
1373
		config.repeatRednetMessages = true
1374
		saveConfig()
1375
		writeLog("Rednet repeating is now on", theme.userResponse, math.huge)
1376
	elseif set == "off" then
1377
		config.repeatRednetMessages = false
1378
		saveConfig()
1379
		writeLog("Rednet repeating is now off", theme.userResponse, math.huge)
1380
	else
1381
		if config.repeatRednetMessages then
1382
			writeLog("Rednet repeating is currently turned on", theme.userResponse, math.huge)
1383
		else
1384
			writeLog("Rednet repeating is currently turned off", theme.userResponse, math.huge)
1385
		end
1386
	end
1387
end
1388
1389
commands["update"] = function()
1390
	term.setCursorPos(1, h)
1391
	term.clearLine()
1392
	term.setTextColor(theme.userResponse)
1393
	term.setCursorBlink(false)
1394
	term.write("Updating...")
1395
	local handle = http.get(updateURL)
1396
	if not handle then
1397
		writeLog("Failed to connect to update server!", theme.error, math.huge)
1398
	else
1399
		data = handle.readAll()
1400
		if #data < 1000 then
1401
			writeLog("Failed to update server!", theme.error, math.huge)
1402
		else
1403
			local f = io.open("/"..shell.getRunningProgram(), "w")
1404
			f:write(data)
1405
			f:close()
1406
			error("firewolf-restart")
1407
		end
1408
	end
1409
end
1410
1411
commands["edit"] = function()
1412
	writeLog("Editing server files", theme.userResponse, math.huge)
1413
	term.setBackgroundColor(colors.black)
1414
	if term.isColor() then
1415
		term.setTextColor(colors.yellow)
1416
	else
1417
		term.setTextColor(colors.white)
1418
	end
1419
	term.clear()
1420
	term.setCursorPos(1, 1)
1421
	print("Use exit to finish editing")
1422
	shell.setDir(serverLocation .. "/" .. domain)
1423
	shell.run("/rom/programs/shell")
1424
	os.queueEvent(resetServerEvent)
1425
end
1426
1427
commands["clear"] = function()
1428
	terminalLog = {}
1429
	term.clear()
1430
	os.queueEvent("firewolf-lock-state-update")
1431
end
1432
1433
helpDocs["password"] = {"Change the lock password", "Usage: password <new-password>"}
1434
helpDocs["lock"] = {"Lock the server with a password"}
1435
helpDocs["exit"] = {"Exits and stops Firewolf Server"}
1436
helpDocs["quit"] = helpDocs["exit"]
1437
helpDocs["stop"] = helpDocs["exit"]
1438
helpDocs["restart"] = {"Fully restarts Firewolf Server"}
1439
helpDocs["reboot"] = helpDocs["restart"]
1440
helpDocs["reload"] = {"Reloads the server and Server API"}
1441
helpDocs["refresh"] = helpDocs["reload"]
1442
helpDocs["clear"] = {"Clears the displayed log"}
1443
helpDocs["rednet"] = {"Whether to allow rednet connections", "Usage: rednet <on or off>"}
1444
helpDocs["startup"] = {"Runs the server for the current domain", "on startup"}
1445
helpDocs["repeat"] = {"Whether to repeat rednet messages", "Usage: repeat <on or off>"}
1446
helpDocs["update"] = {"Updates Firewolf Server"}
1447
helpDocs["edit"] = {"Opens shell in server directory"}
1448
1449
commands["help"] = function(command)
1450
	if command then
1451
		if helpDocs[command] then
1452
			for _, v in pairs(helpDocs[command]) do
1453
				writeLog(v, theme.userResponse, math.huge)
1454
			end
1455
		else
1456
			writeLog("Command does not exist!", theme.userResponse, math.huge)
1457
		end
1458
	else
1459
		writeLog("Use \"help <command>\" for more info", theme.userResponse, math.huge)
1460
		writeLog("Wiki: http://bit.ly/firewolf-wiki", theme.userResponse, math.huge)
1461
		writeLog("Commands: password, lock, exit, update,", theme.userResponse, math.huge)
1462
		writeLog("restart, clear, rednet, repeat, startup,", theme.userResponse, math.huge)
1463
		writeLog("edit, reload", theme.userResponse, math.huge)
1464
	end
1465
end
1466
1467
--  Display manager
1468
1469
local enteredText = ""
1470
local history = {}
1471
local scrollingHistory = false
1472
local cursorPosition = 1
1473
local offsetPosition = 1
1474
local inputName = "> "
1475
local lockTimer = 0
1476
local lockedInputState = false
1477
1478
local lockArt = [[
1479
  ####
1480
 #    #
1481
 #    #
1482
########
1483
--------
1484
########
1485
########
1486
1487
[LOCKED]
1488
]]
1489
1490
local drawBar = function()
1491
	term.setTextColor(theme.barText)
1492
	term.setBackgroundColor(theme.barColor)
1493
	term.setCursorPos(1,1)
1494
	term.clearLine()
1495
	term.write(" "..version)
1496
	center("["..domain.."]")
1497
	local time = textutils.formatTime(os.time(), true)
1498
	term.setCursorPos(w - #time, 1)
1499
	term.setTextColor(theme.clock)
1500
	term.write(time)
1501
end
1502
1503
local drawLogs = function()
1504
	if locked and lastLogNum >= 0 then
1505
		term.setBackgroundColor(theme.background)
1506
		term.clear()
1507
		lastLogNum = -1
1508
		local lockX = math.ceil((w/2) - 4)
1509
		local lineNum = 4
1510
		term.setTextColor(theme.lock)
1511
		for line in lockArt:gmatch("[^\n]+") do
1512
			term.setCursorPos(lockX, lineNum)
1513
			term.write(line)
1514
			lineNum = lineNum + 1
1515
		end
1516
		inputName = "Password: "
1517
	elseif not locked and lastLogNum ~= #terminalLog then
1518
		term.setBackgroundColor(theme.background)
1519
		term.clear()
1520
		lastLogNum = #terminalLog
1521
		lockedInputState = false
1522
		if #terminalLog < h - 1 then
1523
			term.setCursorPos(1, 2)
1524
			for i = 1, #terminalLog do
1525
				term.setTextColor(theme.clock)
1526
				term.write(terminalLog[i][3])
1527
				term.setTextColor(terminalLog[i][2])
1528
				term.write(terminalLog[i][1])
1529
				term.setCursorPos(1, i + 2)
1530
			end
1531
		else
1532
			term.setCursorPos(1, 2)
1533
			for i = 1, h - 1 do
1534
				term.setTextColor(theme.clock)
1535
				term.write(terminalLog[#terminalLog - (h - 1) + i][3])
1536
				term.setTextColor(terminalLog[#terminalLog - (h - 1) + i][2])
1537
				term.write(terminalLog[#terminalLog - (h - 1) + i][1])
1538
				term.setCursorPos(1, i + 1)
1539
			end
1540
		end
1541
	end
1542
end
1543
1544
local drawInputBar = function()
1545
	term.setCursorPos(1, h)
1546
	term.setBackgroundColor(theme.background)
1547
	term.clearLine()
1548
1549
	if lockedInputState then
1550
		term.setCursorBlink(false)
1551
		term.setTextColor(theme.error)
1552
		term.write("Incorrect Password!")
1553
	else
1554
		term.setCursorBlink(true)
1555
		term.setTextColor(theme.inputColor)
1556
		term.write(inputName)
1557
		width = w - #inputName
1558
		if locked then
1559
			term.write(string.rep("*", (#enteredText:sub(offsetPosition, offsetPosition + width))))
1560
		else
1561
			term.write(enteredText:sub(offsetPosition, offsetPosition + width))
1562
		end
1563
	end
1564
end
1565
1566
local handleKeyEvents = function(event, key)
1567
	if key == 14 then
1568
		if enteredText ~= "" then
1569
			enteredText = enteredText:sub(1, cursorPosition - 2) .. enteredText:sub(cursorPosition, -1)
1570
			cursorPosition = cursorPosition-1
1571
			if cursorPosition >= width then
1572
				offsetPosition = offsetPosition - 1
1573
			end
1574
		end
1575
	elseif key == 28 and enteredText ~= "" and locked then
1576
		if enteredText == config.password then
1577
			writeLog("Successful login", theme.userResponse, math.huge)
1578
			inputName = "> "
1579
			locked = false
1580
			os.queueEvent("firewolf-lock-state-update")
1581
		else
1582
			writeLog("Failed login attempt", theme.userResponse, math.huge)
1583
			lockedInputState = true
1584
			lockTimer = os.startTimer(2)
1585
			os.queueEvent("firewolf-lock-state-update")
1586
		end
1587
		enteredText = ""
1588
		cursorPosition = 1
1589
		offsetPosition = 1
1590
	elseif key == 28 and enteredText ~= "" and not locked then
1591
		local commandWord = false
1592
		local arguments = {}
1593
		for word in enteredText:gmatch("%S+") do
1594
			if not commandWord then
1595
				commandWord = word
1596
			else
1597
				table.insert(arguments, word)
1598
			end
1599
		end
1600
		if commands[commandWord] then
1601
			local err, msg = pcall(commands[commandWord], unpack(arguments))
1602
			if not err and msg:find("firewolf-exit", nil, true) then
1603
				error("firewolf-exit")
1604
			elseif not err and msg:find("firewolf-restart", nil, true) then
1605
				error("firewolf-restart")
1606
			elseif not err then
1607
				writeLog("An error occured when executing command", theme.error, math.huge)
1608
				writeError(tostring(msg))
1609
			end
1610
		else
1611
			writeLog("No such command!", theme.error, math.huge)
1612
		end
1613
		table.insert(history, enteredText)
1614
		enteredText = ""
1615
		offsetPosition = 1
1616
		cursorPosition = 1
1617
	elseif key == 203 then
1618
		cursorPosition = cursorPosition - 1
1619
		if cursorPosition < 1 then
1620
			cursorPosition = 1
1621
		end
1622
		if cursorPosition >= width then
1623
			offsetPosition = offsetPosition - 1
1624
		end
1625
	elseif key == 205 then
1626
		cursorPosition = cursorPosition + 1
1627
		if cursorPosition > #enteredText + 1 then
1628
			cursorPosition = #enteredText + 1
1629
		end
1630
		if cursorPosition - offsetPosition >= width then
1631
			offsetPosition = offsetPosition + 1
1632
		end
1633
	elseif key == 208 and #history > 0 then
1634
		if type(scrollingHistory) == "number" then
1635
			scrollingHistory = scrollingHistory - 1
1636
			if scrollingHistory > 0 then
1637
				enteredText = history[#history - scrollingHistory + 1]
1638
				cursorPosition = #enteredText + 1
1639
			else
1640
				scrollingHistory = false
1641
				enteredText = ""
1642
				cursorPosition = 1
1643
			end
1644
		end
1645
	elseif key == 200 and #history > 0 then
1646
		if type(scrollingHistory) == "number" then
1647
			scrollingHistory = scrollingHistory + 1
1648
			if scrollingHistory > #history then
1649
				scrollingHistory = #history
1650
			end
1651
			enteredText = history[#history - scrollingHistory + 1]
1652
			cursorPosition = #enteredText + 1
1653
			if cursorPosition > width then
1654
				cursorPosition = 1
1655
			end
1656
		else
1657
			scrollingHistory = 1
1658
			enteredText = history[#history - scrollingHistory + 1]
1659
			cursorPosition = #enteredText + 1
1660
			if cursorPosition > width then
1661
				cursorPosition = 1
1662
			end
1663
		end
1664
	end
1665
end
1666
1667
1668
local terminalDaemon = function()
1669
	local timer = os.startTimer(1)
1670
	local lastTime = os.clock()
1671
1672
	while true do
1673
		drawLogs()
1674
		drawBar()
1675
		drawInputBar()
1676
1677
		term.setCursorPos(cursorPosition - offsetPosition + #inputName + 1, h)
1678
1679
		local event, key = os.pullEventRaw()
1680
		if event == "char" and not lockedInputState then
1681
			enteredText = enteredText:sub(1, cursorPosition-1) .. key .. enteredText:sub(cursorPosition, -1)
1682
			cursorPosition = cursorPosition + 1
1683
			if cursorPosition - offsetPosition >= width then
1684
				offsetPosition = offsetPosition + 1
1685
			end
1686
		elseif event == "key" and not lockedInputState then
1687
			handleKeyEvents(event, key)
1688
		elseif event == "timer" and key == timer then
1689
			timer = os.startTimer(1)
1690
			lastTime = os.clock()
1691
		elseif event == "timer" and key == lockTimer then
1692
			lockedInputState = false
1693
		elseif event == "terminate" and not locked then
1694
			error("firewolf-exit")
1695
		end
1696
1697
		if (os.clock() - lastTime) > 1 then
1698
			timer = os.startTimer(1)
1699
			lastTime = os.clock()
1700
		end
1701
	end
1702
end
1703
1704
--  Coroutine manager
1705
1706
local receiveThread, responseThread, terminalThread
1707
1708
local loadServerAPI = function()
1709
	if fs.exists(serverLocation .. "/" .. domain .. "/" .. serverAPILocation) and not fs.isDir(serverLocation .. "/" .. domain .. "/" .. serverAPILocation) then
1710
		local f = io.open(serverLocation .. "/" .. domain .. "/" .. serverAPILocation, "r")
1711
		local apiData = f:read("*a")
1712
		f:close()
1713
1714
		customCoroutines = {}
1715
1716
		local apiFunction, err = loadstring(apiData)
1717
		if not apiFunction then
1718
			writeLog("Error while loading server API", theme.error, math.huge)
1719
			if err:match("%[string \"string\"%](.+)") then
1720
				writeLog("server_api" .. err:match("%[string \"string\"%](.+)"), theme.error, math.huge)
1721
			else
1722
				writeLog(err, theme.error, math.huge)
1723
			end
1724
		else
1725
			local global = getfenv(0)
1726
			local env = {}
1727
1728
			for k,v in pairs(global) do
1729
				env[k] = v
1730
			end
1731
1732
			env["server"] = {}
1733
1734
			env["server"]["domain"] = domain
1735
1736
			env["server"]["modem"] = Modem
1737
1738
			env["server"]["handleRequest"] = function(index, func)
1739
				if not(index and func and type(index) == "string" and type(func) == "function") then
1740
					return error("index (string) and handler (function) expected")
1741
				elseif #index:gsub("/", "") == 0 then
1742
					return error("invalid index")
1743
				end
1744
				if index == "/" then
1745
					globalHandler = func
1746
					return
1747
				end
1748
				if index:sub(1, 1) == "/" then index = index:sub(2, -1) end
1749
				if index:sub(-1, -1) == "/" then index = index:sub(1, -2) end
1750
				if index:find(":") then
1751
					return error("Handle index cannot contain \":\"s")
1752
				end
1753
				handles[index] = func
1754
			end
1755
1756
			env["server"]["runCoroutine"] = function(func)
1757
				local newThread = coroutine.create(func)
1758
				local err, msg = coroutine.resume(newThread)
1759
				if not err then
1760
					return error(msg)
1761
				end
1762
1763
				table.insert(customCoroutines, newThread)
1764
			end
1765
1766
			env["server"]["applyTemplate"] = function(variables, template)
1767
				local result
1768
				if fs.exists(template) and not fs.isDir(template) then
1769
					local f = io.open(template, "r")
1770
					result = f:read("*a")
1771
					f:close()
1772
				else
1773
					writeLog("Template file \"" .. template .. "\" does not exist!", theme.error, math.huge)
1774
					writeLog("Template locations are relative to / (root)", theme.error, math.huge)
1775
					return false
1776
				end
1777
				for k,v in pairs(variables) do
1778
					result = result:gsub("{{"..Cryptography.sanatize(k).."}}", Cryptography.sanatize(v))
1779
				end
1780
				return result
1781
			end
1782
1783
			env["server"]["log"] = function(text)
1784
				writeLog(text, theme.notice, math.huge)
1785
			end
1786
1787
			setfenv(apiFunction, env)
1788
			local err, msg = pcall(apiFunction)
1789
1790
			if not err then
1791
				writeLog("Error while executing server API", theme.error, math.huge)
1792
				writeError(tostring(msg))
1793
			else
1794
				writeLog("Server API loaded", theme.notice, math.huge)
1795
			end
1796
		end
1797
	end
1798
end
1799
1800
local function resetServer()
1801
	connections = {}
1802
	repeatStack = {}
1803
	responseStack = {}
1804
	Modem.closeAll()
1805
	Modem.open(serverChannel)
1806
	Modem.open(dnsListenChannel)
1807
	if config.repeatRednetMessages then
1808
		Modem.open(rednet.CHANNEL_REPEAT)
1809
	end
1810
1811
	loadServerAPI()
1812
1813
	responseThread = coroutine.create(function() responseDaemon() end)
1814
	receiveThread = coroutine.create(receiveDaemon)
1815
	coroutine.resume(responseThread)
1816
	coroutine.resume(receiveThread)
1817
	writeLog("The server has been reloaded", theme.userResponse, math.huge)
1818
1819
	os.queueEvent("firewolf-lock-state-update")
1820
end
1821
1822
local function runThreads()
1823
	while true do
1824
		local shouldResetServer = false
1825
		local events = {os.pullEventRaw()}
1826
1827
		err, msg = coroutine.resume(receiveThread, unpack(events))
1828
		if not err then
1829
			writeLog("Internal error!", theme.error, math.huge)
1830
			writeError(tostring(msg))
1831
			shouldResetServer = true
1832
		end
1833
1834
		err, msg = coroutine.resume(responseThread)
1835
		if not err then
1836
			writeLog("Internal error!", theme.error, math.huge)
1837
			writeError(tostring(msg))
1838
			shouldResetServer = true
1839
		end
1840
1841
		err, msg = coroutine.resume(terminalThread, unpack(events))
1842
		if not err and msg:find("firewolf-exit", nil, true) then
1843
			writeLog("Normal exit", theme.text, math.huge)
1844
			Modem.closeAll()
1845
			shell.setDir("")
1846
			term.setBackgroundColor(colors.black)
1847
			term.clear()
1848
			term.setCursorPos(1, 1)
1849
			term.setTextColor(colors.white)
1850
			center("Thank you for using Firewolf Server")
1851
			center("Made by 1lann and GravityScore")
1852
			return
1853
		elseif not err and msg:find("firewolf-restart", nil, true) then
1854
			writeLog("Firewolf server restarting...", theme.text, math.huge)
1855
			term.clear()
1856
			error("firewolf-restart")
1857
		elseif not err then
1858
			term.clear()
1859
			term.setCursorPos(1, 1)
1860
			error("Restart required error: "..msg)
1861
		end
1862
1863
		for k,v in pairs(customCoroutines) do
1864
			if coroutine.status(v) ~= "dead" then
1865
				local err, msg = coroutine.resume(v, unpack(events))
1866
				if not err then
1867
					writeLog("Server API coroutine error!", theme.error, math.huge)
1868
					writeError(tostring(msg))
1869
					shouldResetServer = true
1870
				end
1871
			end
1872
		end
1873
1874
		if events[1] == resetServerEvent or shouldResetServer then
1875
			resetServer()
1876
		end
1877
	end
1878
end
1879
1880
local runOnDomain = function()
1881
	serverChannel = Cryptography.channel(domain)
1882
	requestMatch = header.requestMatchA .. Cryptography.sanatize(domain) .. header.requestMatchB
1883
	responseHeader = header.responseHeaderA .. domain .. header.responseHeaderB
1884
	pageRequestMatch = header.pageRequestMatchA .. Cryptography.sanatize(domain) .. header.pageRequestMatchB
1885
	pageResposneHeader = header.pageResponseHeaderA .. domain .. header.pageResponseHeaderB
1886
	closeMatch = header.closeMatchA .. domain .. header.closeMatchB
1887
	maliciousMatch = header.maliciousMatchA .. Cryptography.sanatize(domain) .. header.maliciousMatchB
1888
1889
	Modem.open(serverChannel)
1890
	Modem.open(dnsListenChannel)
1891
1892
	if config.repeatRednetMessages then
1893
		Modem.open(rednet.CHANNEL_REPEAT)
1894
	end
1895
1896
	writeLog("Firewolf Server "..version.." running" , theme.notice, math.huge)
1897
1898
	loadServerAPI()
1899
1900
	receiveThread = coroutine.create(receiveDaemon)
1901
	responseThread = coroutine.create(responseDaemon)
1902
	terminalThread = coroutine.create(terminalDaemon)
1903
1904
	coroutine.resume(receiveThread)
1905
	coroutine.resume(responseThread)
1906
	coroutine.resume(terminalThread)
1907
1908
	runThreads()
1909
end
1910
1911
--  Server initialisation
1912
1913
local init = function()
1914
	Modem.closeAll()
1915
	term.setBackgroundColor(theme.background)
1916
	term.setTextColor(theme.notice)
1917
	term.clear()
1918
	term.setCursorPos(1,1)
1919
	term.setCursorBlink(false)
1920
	term.setTextColor(theme.text)
1921
1922
	if not fs.exists(serverLocation .. "/" .. domain) then
1923
		makeDirectory(serverLocation .. "/" .. domain)
1924
	else
1925
		if not fs.isDir(serverLocation .. "/" .. domain) then
1926
			fs.delete(serverLocation .. "/" .. domain)
1927
		end
1928
		makeDirectory(serverLocation .. "/" .. domain)
1929
	end
1930
1931
	local report = checkConfig()
1932
	if report then
1933
		term.setBackgroundColor(colors.black)
1934
		term.setTextColor(theme.error)
1935
		term.clear()
1936
		term.setCursorPos(1, 1)
1937
		print("There was an error loading your config file")
1938
		print("The config file is located at: "..configLocation)
1939
		print("-------------------------------------------------")
1940
		for k,v in pairs(report) do
1941
			print(v)
1942
		end
1943
		return
1944
	end
1945
1946
	if config.password then
1947
		locked = true
1948
	end
1949
1950
	local err, msg = pcall(function()runOnDomain()end)
1951
	if not err and msg:find("firewolf-restart", nil, true) then
1952
		term.clear()
1953
		term.setCursorPos(1, 1)
1954
		return shell.run("/"..shell.getRunningProgram(), domain)
1955
	elseif not err then
1956
		term.setBackgroundColor(colors.black)
1957
		term.clear()
1958
		term.setCursorPos(1, 1)
1959
		term.setTextColor(colors.red)
1960
		print("Sorry, Firewolf Server has crashed! Error:")
1961
		print(msg)
1962
		print("Firewolf Server will reboot in 3 seconds...")
1963
		sleep(3)
1964
		return shell.run("/"..shell.getRunningProgram(), domain)
1965
	end
1966
end
1967
1968
if pocket or turtle then
1969
	term.setTextColor(theme.error)
1970
	print("Sorry, Firewolf Server can")
1971
	print("only be ran on computers.")
1972
	return
1973
end
1974
1975
if not Modem.exists() then
1976
	term.setTextColor(theme.error)
1977
	print("Error: No modems found!")
1978
	return
1979
end
1980
1981
if fs.isDir(configLocation) then
1982
	fs.delete(configLocation)
1983
end
1984
1985
if not fs.exists(configLocation) then
1986
	saveConfig()
1987
end
1988
1989
loadConfig()
1990
1991
if not domain and config.lastDomain then
1992
	domain = config.lastDomain
1993
end
1994
1995
if domain then
1996
	if term.isColor() then
1997
		term.setTextColor(colors.yellow)
1998
	else
1999
		term.setTextColor(colors.white)
2000
	end
2001
	term.setCursorBlink(false)
2002
	print("Initializing Firewolf Server...")
2003
	local report = checkDomain(domain)
2004
	term.setTextColor(theme.error)
2005
	if report == "symbols" then
2006
		print("Domain cannot contain \":\", \"/\" and \"?\"s")
2007
	elseif report == "short" then
2008
		print("Domain name too short!")
2009-
	elseif report == "taken" then
2009+
2010-
		print("Domain already taken!")
2010+
2011
		saveConfig()
2012
		init()
2013
	end
2014
else
2015
	term.setTextColor(colors.white)
2016
	print("Usage: "..shell.getRunningProgram().." <domain>")
2017
end