mirror of
https://github.com/tomoko-dev9/nntpchan.git
synced 2026-05-14 05:05:40 +02:00
Compare commits
1246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2466dd83f4 | |||
| 4798813a76 | |||
| aee7b0a2e2 | |||
| 50e8b0a327 | |||
| fafcff9f58 | |||
| 50cb850273 | |||
| 54b605329a | |||
| 6942dad3aa | |||
| 99aeef0156 | |||
| 1d44337dfd | |||
| ed15a54980 | |||
| c3e3113615 | |||
| bfc138bd9a | |||
| 906e8783f3 | |||
| 21c932cbeb | |||
| 52d74995f5 | |||
| 387e57f54e | |||
| fd8c1124f9 | |||
| f0d2cc0b02 | |||
| 67faa7794d | |||
| 28e18703e5 | |||
| ab795ba1ee | |||
| 8903661383 | |||
| fa3b68c708 | |||
| b7e43d3725 | |||
| e0469e8c7f | |||
| cba6de85af | |||
| d4a41db15f | |||
| 5bbcfc8bef | |||
| a2cf5a419b | |||
| aeee8a7e92 | |||
| 922ebd727b | |||
| e7f01ca35c | |||
| 53acf0adf6 | |||
| da10557324 | |||
| c3dec20a57 | |||
| a8cd2a2c47 | |||
| 306bfbaf50 | |||
| 38f12d18aa | |||
| f92f68c3cd | |||
| 77fe66c330 | |||
| ff8c3e915a | |||
| 2f5f84da4b | |||
| 477acabd19 | |||
| e2cbffea30 | |||
| 0261f26043 | |||
| 5381c7b2a4 | |||
| 015c64139d | |||
| 4b08919f75 | |||
| 15ccb7ad50 | |||
| 12709d364b | |||
| 34ce6f805a | |||
| 40a5e9be3f | |||
| 8f39dec91b | |||
| afb98efb2a | |||
| d8f888dffa | |||
| a207f1aaea | |||
| 558dacac79 | |||
| f5d68e17f1 | |||
| c8e3faa4c6 | |||
| 43ce4490ed | |||
| 3f8c583791 | |||
| b4d2de6ec8 | |||
| 0972f86714 | |||
| ab6fe44e8d | |||
| bf43469a89 | |||
| 41682f1712 | |||
| 0176b7f038 | |||
| 3807561ca1 | |||
| cbc8528f4c | |||
| 97fccd342f | |||
| 118f7f4ee0 | |||
| 686cfb7831 | |||
| d051ac77f7 | |||
| 4ae08d2c11 | |||
| 794e2350cd | |||
| 1d0f501968 | |||
| 0252dfa512 | |||
| 8ef4322fba | |||
| 1b03dce124 | |||
| 6807aaee3d | |||
| a00630a6b3 | |||
| 9d43d84926 | |||
| c1afacda17 | |||
| df93bf3f4b | |||
| 88b869c4c8 | |||
| fedc284478 | |||
| f664bc6a9b | |||
| af13a99954 | |||
| 8f7d57f64d | |||
| e13dcf9e20 | |||
| 74ca4caca5 | |||
| 7e1a6cc8f5 | |||
| 04fc996c2d | |||
| 8cad631e73 | |||
| 679382c7f5 | |||
| acb0b350e0 | |||
| de14087362 | |||
| 02e8089668 | |||
| fe68932c7b | |||
| 54b8df91d4 | |||
| 80bf47eec4 | |||
| 67e0f259b6 | |||
| 59068bb961 | |||
| bcddab9af6 | |||
| 31b6f814d4 | |||
| 1a18d20a1a | |||
| 25cb6b7d3f | |||
| 29e6d12967 | |||
| 8fd1f4a30f | |||
| e230c75c9b | |||
| a8695c5caf | |||
| 35122e6459 | |||
| b61741fdda | |||
| 7b64d2eeae | |||
| fb0c600e3a | |||
| ef4d45a148 | |||
| 966c999d68 | |||
| ae9a96a35f | |||
| e2ac75fee6 | |||
| b54e1d84da | |||
| 34be78da8a | |||
| af161968c8 | |||
| a1d11c594c | |||
| 40e4ae1fc4 | |||
| 2ac773cc64 | |||
| 7e6f143108 | |||
| 0994940ae3 | |||
| fa511c275e | |||
| 7e56a9b9f5 | |||
| b5ff2dc4a2 | |||
| 1517941b29 | |||
| 2d62a3bc7f | |||
| 3b492579b8 | |||
| 89d4794871 | |||
| 22eb29c8ee | |||
| 740bf82a1e | |||
| 05ebac9aa5 | |||
| a9f8bf2f8c | |||
| 28e8e95207 | |||
| 0e6e2093e4 | |||
| 053708a9cb | |||
| 4d4aea61fe | |||
| 7dd2228956 | |||
| 2d3c304c81 | |||
| be7eec855a | |||
| 91e758c834 | |||
| 3fb9140a07 | |||
| 9f18416f08 | |||
| 6694c23859 | |||
| fbc53d1e81 | |||
| 76f9d84fa0 | |||
| d1c392ce29 | |||
| cbd7d30e8d | |||
| 613ae771c1 | |||
| 23c357eaac | |||
| 795fcbe37c | |||
| ad07b95d96 | |||
| c89c06e15d | |||
| 57f431ffd2 | |||
| 2265b4b2ae | |||
| 515f42c664 | |||
| 5c4eb739d6 | |||
| c010b3f2c5 | |||
| 57552f53e4 | |||
| 955efe33a1 | |||
| 0e72397956 | |||
| cc4cee1322 | |||
| 5b8326745c | |||
| 95448d82f0 | |||
| 196acdb134 | |||
| 142c40889b | |||
| 9cecd94fc2 | |||
| 0ae8107138 | |||
| 97a1aba125 | |||
| 2a4b5d768a | |||
| 8349cdb74b | |||
| f1adf381ce | |||
| 5c09be1a6d | |||
| a6b15674c6 | |||
| e72a37f928 | |||
| 0c77218b70 | |||
| b4de45569e | |||
| 644f8da3f4 | |||
| 7f42443cce | |||
| 91ced83c3a | |||
| 56c7c5bf21 | |||
| 468320706c | |||
| 4a02015f8e | |||
| 648889e3c5 | |||
| df450e31ca | |||
| 6e4514fed4 | |||
| 880096ea47 | |||
| 4cb037cbf4 | |||
| 15af182415 | |||
| 0ef0a1ee6a | |||
| 92b2550865 | |||
| eeac199b1e | |||
| c34bed4d85 | |||
| 9129dcd916 | |||
| c896ac31c5 | |||
| dba84638c9 | |||
| 695956f9bd | |||
| 8b7b894eb3 | |||
| ec714e03de | |||
| 03e577d04d | |||
| e78e52debb | |||
| c1fd82bab5 | |||
| d5e0f7d698 | |||
| 5ea16f369a | |||
| 9ebc76e4e8 | |||
| 6a69c81e79 | |||
| 7a7432f2b5 | |||
| d58ab2f034 | |||
| 7468a6ed35 | |||
| 1077f0935e | |||
| 852a847c58 | |||
| fd1c4ccb12 | |||
| e5c8ea84d0 | |||
| 1212ae09f4 | |||
| 28c4e9a22d | |||
| 8c4edf27a0 | |||
| ce2ffc1927 | |||
| fe409fe586 | |||
| 1530a0aae5 | |||
| a267c9ac35 | |||
| 6366f626f3 | |||
| b1f88783a6 | |||
| ddb8be8386 | |||
| a674394fa2 | |||
| e78bd50ef1 | |||
| 3b8fd51e53 | |||
| 720aeee7ee | |||
| a42c8abded | |||
| ebe1f96cc5 | |||
| 4244345eff | |||
| 657b285375 | |||
| b75afcb4f4 | |||
| ee770c8ca0 | |||
| ac91e309d9 | |||
| 8263a92432 | |||
| 8604129164 | |||
| f33ee270d5 | |||
| e01fbacf7c | |||
| 8f40d7842a | |||
| 60de45415c | |||
| d370df06e8 | |||
| 28dbe8009d | |||
| 3861e5176d | |||
| 9711298cae | |||
| 5f2aef5fd2 | |||
| 035d4f5406 | |||
| 3996250ee9 | |||
| 05d27962c4 | |||
| 9d33a89bc1 | |||
| 242e996ded | |||
| c503cddd85 | |||
| 5c10ecb2e9 | |||
| cb41b06cb7 | |||
| e08fc8881d | |||
| 8bd528aa50 | |||
| 54573f3cd9 | |||
| 7545efc8d3 | |||
| 7ccd554c2d | |||
| b227bf6ff1 | |||
| 0c41298fe0 | |||
| 4df3bc0672 | |||
| 34fdc0a154 | |||
| bae6e1186c | |||
| 45dfa1c32c | |||
| eef7a0442b | |||
| 4c9c34cb9f | |||
| 1d9f6b09f6 | |||
| 6eba2d4653 | |||
| a70d74e273 | |||
| caf9378073 | |||
| 58464582bd | |||
| 69f868ecb9 | |||
| a0ff118323 | |||
| 43df30c5bf | |||
| 60a12169a0 | |||
| 3899989f2e | |||
| ce7f112be8 | |||
| e6417b3bd7 | |||
| 8a72b29d45 | |||
| fcd5a97225 | |||
| 7c5546c0c0 | |||
| 7abd41eecd | |||
| c5479e2386 | |||
| b61c22898e | |||
| e2de5edd43 | |||
| 2449cb1adc | |||
| 2adcc73d92 | |||
| 55ba1e6c7c | |||
| 4bef3d8964 | |||
| aecd4ca291 | |||
| 222a905c3a | |||
| 777cb0941a | |||
| f06cb1d9a2 | |||
| ef1fc85a8a | |||
| f1d3c0a6b5 | |||
| e8e6812a25 | |||
| 6754947dc2 | |||
| 5683e6eba0 | |||
| 95e96db324 | |||
| 024f773a7c | |||
| 702ab469cd | |||
| 3eb2c0df0d | |||
| 6abc6f4021 | |||
| 6fbf3e9bd7 | |||
| 4af10d59a9 | |||
| ed833024f3 | |||
| ff4cb0a33a | |||
| c3426871d2 | |||
| 2bb4540118 | |||
| 0a77cf1a62 | |||
| e9507505af | |||
| b14a9f709d | |||
| ba74a79409 | |||
| 81653a5415 | |||
| 9f17b7add1 | |||
| 6b039265d9 | |||
| 78bb8577b4 | |||
| 9fba95b58d | |||
| 17e72ce097 | |||
| 8894cf6814 | |||
| 410ef6e430 | |||
| d752312868 | |||
| 3dab2ceb95 | |||
| 337a61dd7f | |||
| 685153f94e | |||
| 8cb044a5e3 | |||
| 91cfa9441e | |||
| f80acbecc2 | |||
| 53522b98eb | |||
| 517338264d | |||
| 2752676013 | |||
| aadb4ae230 | |||
| df021531cb | |||
| 37e6129261 | |||
| 0266deee2b | |||
| 92320aff4c | |||
| 3e6a80f58c | |||
| cdef33af7c | |||
| e0350ecb98 | |||
| 082430df55 | |||
| 63fe1ad1b5 | |||
| 8b35e7cf30 | |||
| cff6f50d63 | |||
| a4c8de953a | |||
| 9c89baf68e | |||
| 29ab733808 | |||
| 9e644eb004 | |||
| a7e72e2aff | |||
| f4640e82c4 | |||
| 95a9c1bda9 | |||
| 936782b616 | |||
| feb43b1ed8 | |||
| e093864ee7 | |||
| a7718e9a0a | |||
| 9230349b30 | |||
| 26d4f5dffb | |||
| a88334e985 | |||
| d3ca9dfa33 | |||
| 58cc8c2365 | |||
| 8cd93abe4f | |||
| a07f4a30d6 | |||
| 69359700b4 | |||
| d2552c5cd1 | |||
| 210a2109a7 | |||
| 2195c0a29c | |||
| 96a931de3e | |||
| c4d5ab431a | |||
| 3ce810775e | |||
| d89a811611 | |||
| c6dab2125e | |||
| b8fa8fff80 | |||
| 78606e95de | |||
| b919c095a8 | |||
| 666549e1e4 | |||
| 99a3403ed8 | |||
| e7c6100691 | |||
| bf357a3461 | |||
| c06b503efd | |||
| cda181e820 | |||
| ba16d5d717 | |||
| de253fb204 | |||
| 835c912053 | |||
| c53d4ba9ee | |||
| b25ce88232 | |||
| 9ef6d119c3 | |||
| 75670aa1b7 | |||
| 6d0ff936ce | |||
| 5367b53570 | |||
| f5fc7c0ff3 | |||
| 38a162e416 | |||
| 3c0122e8a2 | |||
| 55dc52daf2 | |||
| 62bb1b7b32 | |||
| 882da39b87 | |||
| f11f67190c | |||
| 75bc4df7f6 | |||
| 53330eef69 | |||
| f771c8c0d9 | |||
| 2aa1573bee | |||
| 09504ca363 | |||
| 3b83f184ea | |||
| 841c5c6afe | |||
| 85248787fc | |||
| 1f24f03cf5 | |||
| 53ba50541f | |||
| 7c94ff329a | |||
| 2740229d6b | |||
| 5834df6cf2 | |||
| 123057d608 | |||
| 24a25d5e20 | |||
| 311503884d | |||
| 8df0d9bbc2 | |||
| f17f865f79 | |||
| e9c88ffd28 | |||
| c583a03f81 | |||
| 12bb8c4936 | |||
| ec7a17a647 | |||
| c6cc0b17c0 | |||
| 529b1dd0f4 | |||
| 36243cb2eb | |||
| ce2e1eb7a8 | |||
| 78797c680f | |||
| 0848412aa0 | |||
| e25b84c686 | |||
| 579bf619f4 | |||
| e67e7a20bd | |||
| dc2de0fbc9 | |||
| 8cdb070723 | |||
| c54820a198 | |||
| 24fea24b03 | |||
| 93cc2ff803 | |||
| 8038663b0a | |||
| 334ac0e4f2 | |||
| b439795e04 | |||
| 6f00406f99 | |||
| a0495130cc | |||
| 54c0821339 | |||
| b3d65dc5b9 | |||
| 54fde6ae2e | |||
| 2284fac632 | |||
| 6ec930a54a | |||
| 394a4a65e1 | |||
| b823223bd1 | |||
| c951473310 | |||
| 23b4f11e6d | |||
| 93b4558b27 | |||
| 493565257e | |||
| d6bbe584de | |||
| 796d3480ba | |||
| ef024977df | |||
| cbe6af7349 | |||
| dcba36873e | |||
| 065e79026e | |||
| 1fd5588457 | |||
| 203e67a017 | |||
| 86896b6c52 | |||
| 9cc9609ef6 | |||
| c8563f5fb4 | |||
| 5a7eabc0d0 | |||
| d245462373 | |||
| 534e023526 | |||
| 6274fff05b | |||
| 0148aeb6af | |||
| 7371db736d | |||
| 42cc7f26c4 | |||
| be7efb24cd | |||
| 3ee449062e | |||
| 73cf6da65d | |||
| 2f86abe62b | |||
| 713cec5f45 | |||
| da7ad5a7fe | |||
| 5a2f3692cf | |||
| b634fa2665 | |||
| ab37624a7a | |||
| 61c35b7652 | |||
| 24b9076c65 | |||
| 69fac43124 | |||
| dee8c005fd | |||
| a85622854b | |||
| b61012fc43 | |||
| d2887a99b4 | |||
| e224ee7aab | |||
| b75d669f4e | |||
| 02edcaec3d | |||
| 5e40fe9c43 | |||
| e5b3027324 | |||
| 295b2b0362 | |||
| a1f8b35599 | |||
| 1aa6824fd7 | |||
| 1b55b4a213 | |||
| 3097cea3a4 | |||
| 693b399f10 | |||
| 7b5ac6602f | |||
| 683d7b7179 | |||
| d97f1332d6 | |||
| b8e862bbb6 | |||
| 3b0a58d24c | |||
| 2e1b934705 | |||
| 22dc099105 | |||
| 723fa5aff5 | |||
| 57b6b5050e | |||
| 52634b7edd | |||
| 412c2ad4ca | |||
| 7165f6eb4a | |||
| eed8c07ef6 | |||
| ea91db2f58 | |||
| 0cd12cf944 | |||
| eed0fc8001 | |||
| efda3efd56 | |||
| 81a08bb407 | |||
| bb890e716b | |||
| 1a2c5b9e4a | |||
| 358fe300ed | |||
| f4a6988f11 | |||
| f2d854d88f | |||
| 4ede62a667 | |||
| 59ea3da355 | |||
| 2983eb6fbd | |||
| 0870b270cc | |||
| f3e76a1e0f | |||
| 7d56d68d14 | |||
| 7524db96fe | |||
| bd67be0280 | |||
| 5be6c9f7e8 | |||
| e78286cc06 | |||
| fea75f7200 | |||
| d61228215e | |||
| cc5d94ee5f | |||
| 2152cd3246 | |||
| c6a79b8893 | |||
| 838f2b8ca7 | |||
| 88d723219a | |||
| dbb5897305 | |||
| 892a7ea58b | |||
| 64c52e327a | |||
| 86b3d3ce62 | |||
| aa3cf130b3 | |||
| ed2f88c0fc | |||
| 2d090269c5 | |||
| aa637b7cb6 | |||
| 38b24825fd | |||
| bb1b9f427d | |||
| c18152a7ba | |||
| 17e89387b5 | |||
| 89c5773625 | |||
| 1dc800b89c | |||
| c1f9191045 | |||
| bbefe94e8a | |||
| d28f272b94 | |||
| 4770129ad6 | |||
| e87005a178 | |||
| 1e69493eef | |||
| 1a70ff9d92 | |||
| 41a8541660 | |||
| f61470468b | |||
| 23ae28bc71 | |||
| fa5e250595 | |||
| f079a1fee4 | |||
| 5e66346662 | |||
| d48b585fcf | |||
| 32e9e4b3eb | |||
| a60ecff7e3 | |||
| 54b8b60edd | |||
| 2e4c42ff8a | |||
| e9cb9e4f46 | |||
| 1753e2e54b | |||
| c2c2146ad3 | |||
| 1f1dc6a63b | |||
| f07a6faec6 | |||
| 51e0763faf | |||
| 5ea8b1b245 | |||
| aecbe2a5a9 | |||
| cff13becaf | |||
| a06a671415 | |||
| 70feeed809 | |||
| 6af4470473 | |||
| bdabd25867 | |||
| d301021122 | |||
| 741ded6694 | |||
| 2074e49d75 | |||
| 809863c472 | |||
| be54d399f3 | |||
| 76a3288d8c | |||
| 3d4d106554 | |||
| c7da354244 | |||
| acebb66227 | |||
| 3a273ccf08 | |||
| dbc47f8c65 | |||
| c03c8c370e | |||
| a6e5fd13e0 | |||
| 37ebcc2693 | |||
| 31a1109372 | |||
| d241137ded | |||
| b78f044b0b | |||
| 4262f4dc59 | |||
| 3629eb41d9 | |||
| d21efc7fd2 | |||
| 90652f8e7b | |||
| 4469462cb7 | |||
| 37598e187d | |||
| e4a9db3f11 | |||
| 0965d34fbb | |||
| 89c2398a96 | |||
| 279faa56b7 | |||
| 19d75eb917 | |||
| dba185c6aa | |||
| 07e62d2057 | |||
| 2f122529b0 | |||
| 942294317a | |||
| cfaa96b82c | |||
| cc467ac312 | |||
| 0966a247e5 | |||
| 4f3ac9f256 | |||
| e9620558ac | |||
| cac9979280 | |||
| fdb6831064 | |||
| fbafc56b4c | |||
| 9e291b1c5a | |||
| 8d4778c2d7 | |||
| 0abb5882ec | |||
| 96f51c9862 | |||
| 872c5d3757 | |||
| 66c1adfece | |||
| 66655b18d5 | |||
| 0e2c1badcd | |||
| f370a4ccbd | |||
| cff94dc8d8 | |||
| db03367945 | |||
| aca6a0dfa4 | |||
| 238ce08337 | |||
| ba113fa90f | |||
| f9e314d949 | |||
| 359317457b | |||
| 648878235e | |||
| d4ab0ff5d0 | |||
| fff6b502fd | |||
| 0cea9f1255 | |||
| bcc2c3fae7 | |||
| 04a3bb2c39 | |||
| 6beed4053a | |||
| 0ef8b3eb2b | |||
| 70a63ca296 | |||
| 18c9512f5d | |||
| 386ccfdcf0 | |||
| 72d4fdfdbb | |||
| 607ef72af0 | |||
| 6554645428 | |||
| 23bfe82ca3 | |||
| b1be5f67d3 | |||
| 308978914f | |||
| 6a37c6ec87 | |||
| 9ab0f519a3 | |||
| d10fe80739 | |||
| 248dcba2e6 | |||
| e6e2ed4705 | |||
| abdd39bbdf | |||
| 22fe5cf4e5 | |||
| baa8d8d5ac | |||
| 120357dacd | |||
| 0e68b9bc1f | |||
| 9703acb04b | |||
| e4b0990375 | |||
| d548c1014e | |||
| c4ccbad74b | |||
| 490f8f973a | |||
| a143636d6c | |||
| 1da1811d13 | |||
| d084bf6d48 | |||
| e3e086739c | |||
| 4a870a8335 | |||
| caf7b14e80 | |||
| ead00a7148 | |||
| b0524645c2 | |||
| 304cd79d20 | |||
| 3b101236f7 | |||
| b0fd79e415 | |||
| 24db116e4e | |||
| 787da56d16 | |||
| 84e568b753 | |||
| 8c9720f6c5 | |||
| 40d588f5a4 | |||
| f18ece1f00 | |||
| bf35e678da | |||
| 893f1ff9db | |||
| de0f12ebc8 | |||
| 484114872c | |||
| 157a318175 | |||
| 7af1587f33 | |||
| b83b75338c | |||
| eeb57e3ae6 | |||
| 4da6a3cdbb | |||
| 0a7aae55e6 | |||
| 8c5f6e9cde | |||
| d37274ac36 | |||
| 87e790570e | |||
| 5aad791819 | |||
| 00c34b458b | |||
| 69c947774d | |||
| ff4336595b | |||
| 71121a8421 | |||
| 546cf9677d | |||
| e5346917be | |||
| ad3b721134 | |||
| f2e723de4f | |||
| 739f2c2291 | |||
| 066c514bf0 | |||
| 0f05488466 | |||
| f35d7b29e0 | |||
| efcd18bc8a | |||
| 881ca6cdf0 | |||
| 7c6bb5c40d | |||
| e9e46e1f59 | |||
| c965f0f7af | |||
| e02f0fc3fd | |||
| 48cc9a6b6b | |||
| fc64ea0acf | |||
| 7e728a4a0a | |||
| 77d87c0856 | |||
| c871e6ccd3 | |||
| 28a30bdd3d | |||
| a66d31f447 | |||
| f482c8972e | |||
| 8b5952f66b | |||
| fa66537f9e | |||
| 08aaac492e | |||
| 665849e016 | |||
| e87d739392 | |||
| e8fa40c0ca | |||
| 279ca91043 | |||
| af7205e8fe | |||
| ce7170c438 | |||
| 9c60901332 | |||
| f0b3de1c6e | |||
| 3a6cbf9de6 | |||
| eb0ef957a4 | |||
| 665c52fbf6 | |||
| 5f8aa9f993 | |||
| 5a13dc4816 | |||
| ddb07b482b | |||
| 9841bd74e3 | |||
| f35def74c3 | |||
| 297848c8c3 | |||
| e0c35620b0 | |||
| 5575bd2ddc | |||
| 35bc32d3b1 | |||
| 1dbcd224ad | |||
| 6247d0cfff | |||
| bff7dadaa1 | |||
| ccaa23c9d8 | |||
| 6ebb563fbb | |||
| d70639dcb6 | |||
| 05ef278693 | |||
| 20f442f8f2 | |||
| 026c5039a9 | |||
| f83b4340c6 | |||
| 674d7fecb2 | |||
| e6f6d4ef37 | |||
| eb369ca538 | |||
| ccefab982f | |||
| e800f9cfd0 | |||
| ac88b2e083 | |||
| 7749fb9ced | |||
| 2a6ee9bc23 | |||
| cbea6e011e | |||
| 94632b37c9 | |||
| 21008c857a | |||
| 8521d0d490 | |||
| 9e47a4f2c8 | |||
| 7d556b0615 | |||
| c5fa3ad4a2 | |||
| 7c8129c42e | |||
| 4411538252 | |||
| 5ab54ae654 | |||
| f0790204c0 | |||
| c331570e37 | |||
| 7ba642df36 | |||
| b29446bd9d | |||
| 836d1212cc | |||
| 461721d729 | |||
| 810c4f52fe | |||
| b0c4dd5f66 | |||
| 4c6cfefdb5 | |||
| 3d195510ba | |||
| 6e5c34ebdd | |||
| 749e1fc069 | |||
| 3f6c6ed615 | |||
| af6c0ba6b8 | |||
| f8acfaaae4 | |||
| da43536e6c | |||
| f4ee1d3e0b | |||
| c7be368be9 | |||
| 3cb06d572e | |||
| f94ecbdaa9 | |||
| f1f9ae33d9 | |||
| 008cfb6db5 | |||
| 46e8f48f66 | |||
| 855c78a8f8 | |||
| 9ec1c5c304 | |||
| 0ab6dca181 | |||
| 1d20cb3142 | |||
| baf6e29473 | |||
| 1346eb56ab | |||
| a8ac1dd67c | |||
| 0e51b675c2 | |||
| 2fd58bebbb | |||
| f44430e3ca | |||
| e705bbccba | |||
| a3ed5a9d91 | |||
| 2012e6d3a8 | |||
| 90bf96a025 | |||
| da9e0d5808 | |||
| 99ebddc7c4 | |||
| baeabad1a4 | |||
| 13724b751a | |||
| cba3c29328 | |||
| 0e1e614696 | |||
| 178b7e56f6 | |||
| 0feb319236 | |||
| 4c927ff74a | |||
| a5f3188cce | |||
| 7cf55931b7 | |||
| 1174df8344 | |||
| b41f1bebc8 | |||
| ccb750679a | |||
| 5b4bcd7cd9 | |||
| a729848963 | |||
| 5727543e28 | |||
| e2f509641a | |||
| d000fcc993 | |||
| 52d401270f | |||
| 4d2d62681b | |||
| 3c61aad46c | |||
| 718863a2fc | |||
| 4d954861aa | |||
| aafe3ad2a8 | |||
| 0c15c22ca2 | |||
| ebf3f578b0 | |||
| c8146eb783 | |||
| 1b18fc89a2 | |||
| cfa83a7db8 | |||
| 8d6bd12346 | |||
| 898bc668d9 | |||
| 171d3e75f4 | |||
| f04627ff4f | |||
| c1c2ed2dda | |||
| 0fb9f392d7 | |||
| 7ac7399dcf | |||
| 2885ebee81 | |||
| ba5801f920 | |||
| b65e0cdfba | |||
| 172b1e7c8c | |||
| 0e8d277482 | |||
| 4be8c78aa7 | |||
| 3e178c63eb | |||
| 5cc2b1b937 | |||
| 997fad55e4 | |||
| 95b4dd7c91 | |||
| 4305692727 | |||
| 4665c0eb4e | |||
| 2aac424dc2 | |||
| e2d7846d45 | |||
| b42a2ae138 | |||
| 91476b700c | |||
| 13f8a0ab13 | |||
| 1467979dab | |||
| dfdf4043f1 | |||
| ba42a523ff | |||
| 49ef756672 | |||
| a97c3d7d48 | |||
| ad3089728a | |||
| bfd16332f1 | |||
| 9cf7e617cb | |||
| 12f8ee9131 | |||
| eabe6474de | |||
| 75dc4c2529 | |||
| 11b5ca6a1b | |||
| 8c2ddda51a | |||
| 963637e750 | |||
| ff8bdcf08c | |||
| 6af49f6800 | |||
| f0a9a67c29 | |||
| 612c9c8989 | |||
| 026eaa2f84 | |||
| f229ea555c | |||
| aa29679a92 | |||
| 97eec6053d | |||
| 64a78a4131 | |||
| c8ca9dd855 | |||
| caddb84dfb | |||
| f9d4d30941 | |||
| a09f1cd454 | |||
| 36c0bc127f | |||
| a36e523ddf | |||
| 992dfad0dc | |||
| 19452704fc | |||
| 953042578b | |||
| 01813b2877 | |||
| 7096ea4e52 | |||
| 9e14d6ce30 | |||
| 60d226c66b | |||
| fc2c582627 | |||
| 0eac006d52 | |||
| 228af0c5f4 | |||
| 0f60daf299 | |||
| 1ec4663499 | |||
| 0e394ae25e | |||
| e818547661 | |||
| 7e2d9f2d0f | |||
| 25ae491ddc | |||
| 5ef0085b57 | |||
| 536b1161c9 | |||
| e35adda727 | |||
| ccbb348155 | |||
| 1e81129090 | |||
| cbad613621 | |||
| 0d5fded584 | |||
| 595a258331 | |||
| d4305eb2f0 | |||
| 34559db9aa | |||
| f275ceaec1 | |||
| 0854260d43 | |||
| 6f8862b631 | |||
| 69c20952ad | |||
| ada1a614ea | |||
| b0ede42c61 | |||
| c9f5cf183c | |||
| e9e94b17d5 | |||
| 23390b5998 | |||
| 8f9622dca8 | |||
| 579970c079 | |||
| 000fb43159 | |||
| 78e7895c70 | |||
| 82a0746d3e | |||
| b5580bebdd | |||
| e672f412dc | |||
| bdf11b535c | |||
| 2468083ba2 | |||
| a00a902022 | |||
| 1465b99df6 | |||
| 38354318b8 | |||
| f319d8e809 | |||
| a86345aa8b | |||
| fc1e20bd6f | |||
| 2b811a6b34 | |||
| f5520a7127 | |||
| a93f3201f9 | |||
| c0ae3d0756 | |||
| 91406d3400 | |||
| 8dd77dcdf0 | |||
| 3ab022f45d | |||
| c677cebde5 | |||
| 6f01bac76c | |||
| c0e216a390 | |||
| 2527e695dd | |||
| fdedd6a6c0 | |||
| 59c52f775f | |||
| c8cb42edfd | |||
| 4ad781e541 | |||
| 4f0c2e2f18 | |||
| 6065eca82f | |||
| 3983506fe3 | |||
| 4ea96b155f | |||
| 3429ac63f8 | |||
| 2ae4f2268b | |||
| 801272ae7d | |||
| d1a81f319d | |||
| 9f7de85a1f | |||
| 518a32016b | |||
| e036db805e | |||
| 56b90bf5f7 | |||
| 6cdcc4c3b9 | |||
| 487495f9ac | |||
| 15bc796952 | |||
| 774b2a5e50 | |||
| 78dccbfd61 | |||
| 6eefd675e6 | |||
| 3fed1d8d41 | |||
| 7a4c875fd5 | |||
| f315823dcd | |||
| 64a9e471ef | |||
| 670f613596 | |||
| 84092c979f | |||
| 3fad9794da | |||
| ecf8237c97 | |||
| c3224379ca | |||
| 2a744c45a2 | |||
| 64b345a7f0 | |||
| f25f2cd956 | |||
| 14c68abf9d | |||
| 3c2da5f25b | |||
| dafa5ca517 | |||
| d81e709827 | |||
| 4c7dd44815 | |||
| 514064ce5a | |||
| dac258a978 | |||
| a9860d82ba | |||
| cc957f3de5 | |||
| 8665b98452 | |||
| 31a12185f6 | |||
| e686f0d57e | |||
| 8ffc8c006c | |||
| a19d36f883 | |||
| 3aa87e07f2 | |||
| be4fbda2a6 | |||
| 7f25dcf95c | |||
| a7e33a9f10 | |||
| 2c49987c6d | |||
| 00fa6eb561 | |||
| 9d18d59f45 | |||
| f47b181290 | |||
| 6d7f2bd587 | |||
| 3bc2befb88 | |||
| 239ba55f1f | |||
| 349c588bdd | |||
| 4bd8f0a688 | |||
| 6b3fc260a6 | |||
| 975693d241 | |||
| 6b934ebd52 | |||
| 95ab7448be | |||
| 1acad1b11f | |||
| 4d91cbd084 | |||
| 89d20a1617 | |||
| 20120a8690 | |||
| 864a751130 | |||
| e668cdc16e | |||
| f76ba6422c | |||
| 9a6400e15b | |||
| a068fe634d | |||
| a5da6d2e78 | |||
| e32f60e658 | |||
| 21b5a21008 | |||
| 4ada177ae9 | |||
| 176c162781 | |||
| d77b2dbff9 | |||
| 6bb2b154ef | |||
| 83cc63fc06 | |||
| 89a004e8ec | |||
| b393c066b0 | |||
| 2eecfeafaa | |||
| 25a15100a9 | |||
| aeab87cb51 | |||
| 458a1c04d9 | |||
| af4baa42a4 | |||
| d64be9150d | |||
| 4be03b0b96 | |||
| b34b58a0cf | |||
| d52c65828f | |||
| 26a21fe6bf | |||
| 030460e2c4 | |||
| 84cdf459ae | |||
| 190321b4c4 | |||
| 37119a249e | |||
| 2caf487c72 | |||
| aecb4b6ac0 | |||
| 554d2d753e | |||
| 13739363f4 | |||
| 00fdbb9449 | |||
| 4369765253 | |||
| 9883ed4396 | |||
| ba7140053a | |||
| 2727fe9dff | |||
| e080e939dd | |||
| 4b1da71030 | |||
| 2aa79bd014 | |||
| de5883d0a0 | |||
| ce3919fc1f | |||
| 561a0156be | |||
| fde7ed3d3b | |||
| 3c4ad2fe50 | |||
| cd803852e7 | |||
| 2e2968e188 | |||
| 4f3bc5cf6e | |||
| 378a257377 | |||
| 04df5de9a1 | |||
| 9ae0b0ef5b | |||
| d31ca5b6a7 | |||
| cc089b3401 | |||
| 7b8d33826b | |||
| 9e9a1efe06 | |||
| a5d7cb9074 | |||
| 053b5870cf | |||
| 4ab90f3152 | |||
| dc75a3513e | |||
| 40ceb747ae | |||
| 6996e3abc4 | |||
| 5118ffb3f8 | |||
| 287a49f196 | |||
| 698ed1d42e | |||
| a0deb532e3 | |||
| 0e1e6201ca | |||
| 6ea75236fa | |||
| 61281190bb | |||
| 2355528b46 | |||
| db5b8ec309 | |||
| a0a0a6feca | |||
| 787bef7625 | |||
| e57d4ea921 | |||
| c6c9c3f53b | |||
| f7eb634aa1 | |||
| 74f07c3a6c | |||
| 95864559fb | |||
| 5155205e8c | |||
| d91b0d3e99 | |||
| 3df355abdb | |||
| 850e5bccae | |||
| 713fe21190 | |||
| 3d683ab87e | |||
| a4053f0f1a | |||
| 87e29cc117 | |||
| c17d0fb0bb | |||
| 985863deb2 | |||
| a9d2c0425d | |||
| 76cb2341ce | |||
| ceb6f09a3f | |||
| adf3540556 | |||
| 2ce8a38b0b | |||
| e62d959b0b | |||
| 4216a777a3 | |||
| 5873b41e00 | |||
| 14635e9472 | |||
| 422f3de11d | |||
| 64b5531fed | |||
| 17f3b25e51 | |||
| 60f0c19a9f | |||
| f4f2479b8a | |||
| 3506d7f975 | |||
| 9acca366d0 | |||
| 70fd791545 | |||
| 9f3c41fb0b | |||
| 2ff6088605 | |||
| 54644e0da2 | |||
| dbd05c30c3 | |||
| 587cdf4e75 | |||
| 76d75b260f | |||
| 35ff3759f4 | |||
| 671db6e483 | |||
| 32566bfd84 | |||
| 4b02ad6d89 | |||
| a5c2f8d86f | |||
| 6e1e7440dc | |||
| 0b460eeb5d | |||
| 2a32bb75d5 | |||
| cf91d8ffa5 | |||
| 375713390e | |||
| f823b0e612 | |||
| cb1821189a | |||
| 1a5862213c | |||
| 96c019324d | |||
| 1e905b6bbf | |||
| 7c6726b5fc | |||
| 24d18ff599 | |||
| 16d0489cd7 | |||
| 5875347966 | |||
| b2c1d9eb26 | |||
| a0bc39aaa5 | |||
| b887d07172 | |||
| 1f48c58f6b | |||
| e8eec5862f | |||
| bf7213bf09 | |||
| b2201c4ef7 | |||
| a4747b9965 | |||
| 5ec4c823fe | |||
| 00d0be952f | |||
| c448777fe9 | |||
| 30b54c17fa | |||
| e954c57da6 | |||
| d5d30893e5 | |||
| 72947dbbcc | |||
| 1c632666ab | |||
| 5cf12f37a5 | |||
| 9c038021b5 | |||
| 4a8861761d | |||
| 13e0010419 | |||
| e461462bd6 | |||
| 8101e67b10 | |||
| 63edd14243 | |||
| d2142f7c83 | |||
| 28b1864841 | |||
| 5fa1e76971 | |||
| 40eeb116de | |||
| a502259ba9 | |||
| cac18a5e44 | |||
| c86f573440 | |||
| 6f5130d8fd | |||
| 17b8101349 | |||
| 5e53ae6f04 | |||
| 50c395aad4 | |||
| 18bf12e556 | |||
| bc1c038468 | |||
| 7117a25f9a | |||
| ef69bda0d7 | |||
| ab6ac43851 | |||
| 28feb5a84e | |||
| 9e3e0cc443 | |||
| c573fb646b | |||
| 93b3e0f83f | |||
| 7dda235ab1 | |||
| 09e7969479 | |||
| cb37a45b67 | |||
| e25d6ab370 | |||
| e2194d3fbd | |||
| fdc4234d08 | |||
| 0c0160da6d | |||
| fd82218e5f | |||
| 091d74af52 | |||
| e690b7dd9c | |||
| a856af693f | |||
| 3668eb7822 | |||
| 3cc04a6cad | |||
| e6967a75e7 | |||
| 679ee50342 | |||
| b73c219e6b | |||
| 1b474af875 | |||
| 77a72c5770 | |||
| a52fe21743 | |||
| 42c70990a0 | |||
| 0980903b6e | |||
| 47737dd205 | |||
| a22330f920 | |||
| 05b4695398 | |||
| 415fb3c4a8 | |||
| 5138c31e58 | |||
| 052259c9d1 | |||
| 7276e28bb7 | |||
| 2adf2d5127 | |||
| 242094193f | |||
| 2ffc7a80e6 | |||
| 3da1ab157d | |||
| 17d2654f26 | |||
| f155b0e9a1 | |||
| c97296700f | |||
| 3951432bad | |||
| da5c44d4fd | |||
| a35889ca18 | |||
| c171e62d52 | |||
| 75a3cf5ede | |||
| 2d25b349f7 | |||
| 29149eb452 | |||
| 57232aeaf0 | |||
| 2ced2b73c7 |
@@ -0,0 +1,5 @@
|
||||
|
||||
* be awesome to each other
|
||||
* fun is required, don't be a killjoy
|
||||
* **vendor everything**
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
## info
|
||||
|
||||
git revision / version: [git revision or version here]
|
||||
|
||||
OS: [os here]
|
||||
|
||||
Architecture: [architecture here]
|
||||
|
||||
## problem
|
||||
|
||||
[insert description of problem here]
|
||||
|
||||
## backtrace / error messages
|
||||
|
||||
Error messages: [yes/no]
|
||||
|
||||
[insert any error messages here]
|
||||
|
||||
Backtrace: [yes/no]
|
||||
|
||||
[insert any backtraces here]
|
||||
+4
-3
@@ -19,23 +19,24 @@ webroot
|
||||
|
||||
# built binaries
|
||||
go
|
||||
srndv2
|
||||
gopherjs_go
|
||||
./srndv2
|
||||
|
||||
# private key
|
||||
*.key
|
||||
*.txt
|
||||
|
||||
# certificates
|
||||
certs
|
||||
|
||||
|
||||
rebuild.sh
|
||||
vendor
|
||||
.gx
|
||||
|
||||
# generated js
|
||||
contrib/static/nntpchan.js
|
||||
contrib/static/js/nntpchan.js
|
||||
contrib/static/miner-js.js
|
||||
|
||||
|
||||
#docs trash
|
||||
doc/.trash
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#
|
||||
# .gxignore for nntpchan repo
|
||||
#
|
||||
|
||||
# emacs temp files
|
||||
*~
|
||||
\#*
|
||||
.\#*
|
||||
|
||||
# srnd config files
|
||||
srnd.ini
|
||||
feeds.ini
|
||||
|
||||
# default article store directory
|
||||
articles/
|
||||
|
||||
# generated files
|
||||
webroot/
|
||||
|
||||
# built binaries
|
||||
go/
|
||||
srndv2
|
||||
nntpchan
|
||||
|
||||
# private key
|
||||
*.key
|
||||
*.txt
|
||||
|
||||
# certificates
|
||||
certs/
|
||||
|
||||
rebuild.sh
|
||||
|
||||
.git
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
**You have been visited by the CoC(K)**
|
||||
|
||||
|
||||
_.+._
|
||||
(^\/^\/^)
|
||||
\@☆@☆@/
|
||||
CoC(k) {_____}
|
||||
Code Of Conduct Killer _oOPPYbo.
|
||||
_,ooO8O' `Ob
|
||||
_,ooOPP"' Ob dO
|
||||
_oooOP"'' `Oo. ,O[
|
||||
Ob _,ooOPP'' `YYboOP
|
||||
`O[ _ooOP"'' _,oOPP"'
|
||||
YOooooOP' _ooOP"'
|
||||
'' ,ooOP''
|
||||
,odPP''
|
||||
_,oOP'
|
||||
ooOP"'
|
||||
_oOP'o
|
||||
,OP YOL
|
||||
,O. ,OP Yb contribute code or you're a racist
|
||||
dO' " Yb get offended as a responsible adult
|
||||
]O. dO spread this like happy herpes
|
||||
Ob _,o. dOP
|
||||
`Ooo___ooOP'`YbooodPP just imagine what would happen
|
||||
'`"""'' `''' if we all decided to understand
|
||||
|
||||
|
||||
Sweet blessing be upon you but **ONLY** if comment `ebin` to this commit.
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Jeff Becker
|
||||
Copyright (c) 2015-2020 Jeff Becker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
REPO=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
REPO_GOPATH=$(REPO)/go
|
||||
MINIFY=$(REPO_GOPATH)/bin/minify
|
||||
STATIC_DIR=$(REPO)/contrib/static
|
||||
JS=$(STATIC_DIR)/nntpchan.js
|
||||
MINER_JS=$(STATIC_DIR)/miner-js.js
|
||||
CONTRIB_JS=$(REPO)/contrib/js/contrib
|
||||
LOCAL_JS=$(REPO)/contrib/js/nntpchan
|
||||
VENDOR_JS=$(REPO)/contrib/js/vendor
|
||||
SRND_DIR=$(REPO)/contrib/backends/srndv2
|
||||
NNTPCHAND_DIR=$(REPO)/contrib/backends/nntpchand
|
||||
NNTPCHAN_DAEMON_DIR=$(REPO)/contrib/backends/nntpchan-daemon
|
||||
SRND=$(REPO)/srndv2
|
||||
NNTPCHAND=$(REPO)/nntpchand
|
||||
NNTPD=$(REPO)/nntpd
|
||||
|
||||
GOROOT=$(shell go env GOROOT)
|
||||
GO=$(GOROOT)/bin/go
|
||||
|
||||
GOPHERJS_GOROOT ?= $(GOROOT)
|
||||
GOPHERJS_GO = $(GOPHERJS_GOROOT)/bin/go
|
||||
|
||||
GOPHERJS_GOPATH=$(REPO)/gopherjs_go
|
||||
GOPHERJS=$(GOPHERJS_GOPATH)/bin/gopherjs
|
||||
|
||||
all: clean build
|
||||
|
||||
build: js srnd
|
||||
|
||||
full: clean full-build
|
||||
|
||||
full-build: srnd beta native
|
||||
|
||||
js: $(JS)
|
||||
|
||||
srnd: $(SRND)
|
||||
|
||||
$(MINIFY):
|
||||
GO111MODULE=on GOPATH=$(REPO_GOPATH) go get -u github.com/tdewolff/minify
|
||||
|
||||
|
||||
$(GOPHERJS):
|
||||
GOROOT=$(GOPHERJS_GOROOT) GOPATH=$(GOPHERJS_GOPATH) $(GOPHERJS_GO) get -v github.com/gopherjs/gopherjs
|
||||
|
||||
js-deps: $(MINIFY)
|
||||
|
||||
$(MINER_JS): $(GOPHERJS) $(MINIFY)
|
||||
rm -rf $(GOPHERJS_GOPATH)/pkg/
|
||||
cp -rf $(SRND_DIR)/src/github.com $(GOPHERJS_GOPATH)/src/
|
||||
GOROOT=$(GOPHERJS_GOROOT) GOPATH=$(GOPHERJS_GOPATH) $(GOPHERJS) -m -v build github.com/ZiRo-/cuckgo/miner_js -o miner.js
|
||||
$(MINIFY) --mime=text/javascript > $(STATIC_DIR)/miner-js.js < miner.js
|
||||
rm -f miner.js.map miner.js
|
||||
|
||||
$(JS): js-deps
|
||||
|
||||
|
||||
$(SRND):
|
||||
GOROOT=$(GOROOT) $(MAKE) -C $(SRND_DIR)
|
||||
cp $(SRND_DIR)/srndv2 $(SRND)
|
||||
|
||||
beta: $(NNTPCHAND)
|
||||
|
||||
$(NNTPCHAND):
|
||||
GOROOT=$(GOROOT) $(MAKE) -C $(NNTPCHAND_DIR)
|
||||
cp $(NNTPCHAND_DIR)/nntpchand $(NNTPCHAND)
|
||||
|
||||
native: $(NNTPD)
|
||||
|
||||
$(NNTPD):
|
||||
$(MAKE) -C $(NNTPCHAN_DAEMON_DIR)
|
||||
cp $(NNTPCHAN_DAEMON_DIR)/nntpd $(NNTPD)
|
||||
|
||||
test: test-srnd
|
||||
|
||||
test-full: test-srnd test-beta test-native
|
||||
|
||||
test-srnd:
|
||||
GOROOT=$(GOROOT) $(MAKE) -C $(SRND_DIR) test
|
||||
|
||||
test-beta:
|
||||
GOROOT=$(GOROOT) $(MAKE) -C $(NNTPCHAND_DIR) test
|
||||
|
||||
test-native:
|
||||
GOROOT=$(GOROOT) $(MAKE) -C $(NNTPCHAN_DAEMON_DIR) test
|
||||
|
||||
|
||||
clean: clean-srnd clean-js
|
||||
|
||||
clean-full: clean clean-beta clean-native clean-js
|
||||
|
||||
clean-srnd:
|
||||
rm -f $(SRND)
|
||||
GOROOT=$(GOROOT) $(MAKE) -C $(SRND_DIR) clean
|
||||
|
||||
clean-js:
|
||||
rm -f $(JS) $(MINER_JS)
|
||||
|
||||
clean-beta:
|
||||
rm -f $(NNTPCHAND)
|
||||
GOROOT=$(GOROOT) $(MAKE) -C $(NNTPCHAND_DIR) clean
|
||||
|
||||
clean-native:
|
||||
rm -f $(NNTPD)
|
||||
$(MAKE) -C $(NNTPCHAN_DAEMON_DIR) clean
|
||||
|
||||
distclean: clean
|
||||
rm -rf $(REPO_GOPATH)
|
||||
rm -rf $(GOPHERJS_GOPATH)
|
||||
@@ -1,39 +1,233 @@
|
||||
NNTPChan
|
||||
========
|
||||

|
||||

|
||||

|
||||
|
||||
**NNTPChan** (previously known as overchan) is a decentralized imageboard that uses the [NNTP protocol](https://en.wikipedia.org/wiki/Network_News_Transfer_Protocol) (network-news protocol) to synchronize content between many different servers. It utilizes cryptographically signed posts to perform optional/opt-in decentralized moderation.
|
||||
**NNTPChan** (previously known as overchan) is a decentralized imageboard that uses the [NNTP protocol](https://en.wikipedia.org/wiki/Network_News_Transfer_Protocol) (network-news transfer protocol) to synchronize content between many different servers. It utilizes cryptographically signed posts to perform optional/opt-in decentralized moderation.
|
||||
|
||||
This repository contains resources used by the core daemon which is located on [GitHub](https://github.com/majestrate/srndv2) (for now) along with general documentation, [here](doc/)
|
||||
## Getting started — Ubuntu 24.04 installation guide
|
||||
|
||||
##Getting started
|
||||
Tested and working on Ubuntu 24.04.
|
||||
|
||||
[This](doc) is a step-by-step guide for getting up-and-running with NNTPChan as well as documentation for developers wwho want to either work on NNTPChan directly or use NNTPChan in their aplications with the API.
|
||||
### Step 1: Install dependencies
|
||||
|
||||
##Bugs and issues
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get --no-install-recommends install -y \
|
||||
imagemagick ffmpeg sox build-essential git ca-certificates \
|
||||
postgresql postgresql-client golang
|
||||
```
|
||||
|
||||
*PLEASE* report any bugs you find while building, setting-up or using NNTPChan on the [GitHub issue tracker](https://github.com/majestrate/nntpchan/issues) or on the [GitGud issue tracker](https://gitgud.io/uguu/nntpchan/issues) so that the probelms can be resolved or discussed.
|
||||
### Step 2: Install Go 1.23+
|
||||
|
||||
##Active NNTPChan nodes
|
||||
Ubuntu 24.04 ships Go 1.22 which is too old. Install 1.23+ manually:
|
||||
|
||||
Below is a list of known NNTPChan nodes:
|
||||
```bash
|
||||
curl -L -o /tmp/go1.23.0.tar.gz https://dl.google.com/go/go1.23.0.linux-amd64.tar.gz
|
||||
sudo tar -C /usr/local -xzf /tmp/go1.23.0.tar.gz
|
||||
export PATH=/usr/local/go/bin:$PATH
|
||||
go version # should show go1.23.x
|
||||
```
|
||||
|
||||
1. [2hu-ch.org](https://2hu-ch.org)
|
||||
2. [nsfl.tk](https://nsfl.tk)
|
||||
3. [i2p.rocks](https://i2p.rocks/ib/)
|
||||
Add to your shell profile to persist:
|
||||
|
||||
##Support
|
||||
```bash
|
||||
echo 'export PATH=/usr/local/go/bin:$PATH' >> ~/.bashrc
|
||||
```
|
||||
|
||||
Need help? Join us on IRC.
|
||||
### Step 3: Get the NNTPChan source
|
||||
|
||||
1. [freenode: #nntpchan](https://webchat.freenode.net/?channels=#nntpchan)
|
||||
2. [rizon: #nntpchan](https://qchat.rizon.net/?channels=#nntpchan) - Most active
|
||||
```bash
|
||||
git clone https://github.com/tomoko-dev9/nntpchan
|
||||
cd nntpchan
|
||||
```
|
||||
|
||||
##Donations
|
||||
### Step 4: Build minify
|
||||
|
||||
Like this project? Why not help by funding it?
|
||||
The Makefile uses old `go get` syntax. Install minify manually:
|
||||
|
||||
Bitcoin: [15yuMzuueV8y5vPQQ39ZqQVz5Ey98DNrjE](bitcoin://15yuMzuueV8y5vPQQ39ZqQVz5Ey98DNrjE)
|
||||
```bash
|
||||
GONOSUMDB=* GOPROXY=direct GOPATH=/root/nntpchan/go go install github.com/tdewolff/minify/v2/cmd/minify@latest
|
||||
```
|
||||
|
||||
##Acknowledgements
|
||||
### Step 5: Fix the Makefile for modern Go
|
||||
|
||||
* [Deavmi](deavmi.carteronline.net/~deavmi) - Making the documentation beautiful.
|
||||
```bash
|
||||
sed -i 's|GOPATH=$(REPO)|GO111MODULE=off GOPATH=/tmp/srnd-gopath|g' \
|
||||
/root/nntpchan/contrib/backends/srndv2/Makefile
|
||||
```
|
||||
|
||||
Set up the GOPATH symlink:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/srnd-gopath/src
|
||||
ln -s /root/nntpchan/contrib/backends/srndv2/src/srnd /tmp/srnd-gopath/src/srnd
|
||||
```
|
||||
|
||||
### Step 6: Compile
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
If the srndv2 step fails, build it manually:
|
||||
|
||||
```bash
|
||||
cd /root/nntpchan/contrib/backends/srndv2
|
||||
GO111MODULE=off GOPATH=/tmp/srnd-gopath GOROOT=/usr/local/go /usr/local/go/bin/go build -v -o /root/nntpchan/srndv2 .
|
||||
```
|
||||
|
||||
### Step 7: Build the JavaScript bundle
|
||||
|
||||
```bash
|
||||
curl -o /tmp/jquery.js https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
|
||||
|
||||
cat /tmp/jquery.js \
|
||||
/root/nntpchan/contrib/js/entry.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/local_storage.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/api.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/banner.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/theme.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/expand-image.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/expand-video.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/hide-post.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/post-reply.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/reply.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/report.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/captcha-reload.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/crypto.js \
|
||||
/root/nntpchan/contrib/js/nntpchan/livechan.js \
|
||||
> /root/nntpchan/contrib/static/nntpchan.js
|
||||
```
|
||||
|
||||
### Step 8: Set up PostgreSQL
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql -c "CREATE DATABASE root;"
|
||||
sudo -u postgres psql -c "CREATE USER root WITH PASSWORD 'root';"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE root TO root;"
|
||||
sudo -u postgres psql -d root -c "GRANT ALL ON SCHEMA public TO root;"
|
||||
sudo -u postgres psql -d root -c "ALTER SCHEMA public OWNER TO root;"
|
||||
```
|
||||
|
||||
> **Note:** Default database, user, and password are all `root`. Default port is `5432`.
|
||||
|
||||
### Step 9: Initial setup
|
||||
|
||||
```bash
|
||||
cd /root/nntpchan
|
||||
./srndv2 setup
|
||||
```
|
||||
|
||||
Edit `srnd.ini` as needed. To disable captcha add `rapeme=omgyesplz` under `[frontend]`. To enable captcha only for new threads (not replies), leave it as `rapeme=no`.
|
||||
|
||||
### Step 10: Generate admin keypair
|
||||
|
||||
```bash
|
||||
./srndv2 tool keygen
|
||||
```
|
||||
|
||||
Save the secret key securely. Add the public key as admin:
|
||||
|
||||
```bash
|
||||
./srndv2 tool mod add YOUR_PUBLIC_KEY
|
||||
```
|
||||
|
||||
To post as admin, use your secret key in the name field:
|
||||
|
||||
```
|
||||
Admin#YOUR_SECRET_KEY
|
||||
```
|
||||
|
||||
To show admin styling on posts, add this to your theme CSS (e.g. `krane.css`):
|
||||
|
||||
```css
|
||||
[data-pubkey="YOUR_PUBLIC_KEY"] {
|
||||
background-image: url('/static/admin.png');
|
||||
background-size: 25%;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 11: Run as a systemd service
|
||||
|
||||
```bash
|
||||
cat > /etc/systemd/system/nntpchan.service << 'SERVICE'
|
||||
[Unit]
|
||||
Description=nntpchan srndv2
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/root/nntpchan
|
||||
ExecStart=/root/nntpchan/srndv2 run
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICE
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable nntpchan
|
||||
systemctl start nntpchan
|
||||
```
|
||||
|
||||
The web interface is available at `http://127.0.0.1:18000` by default (configurable in `srnd.ini`).
|
||||
|
||||
### Step 12: Set up .onion address (optional)
|
||||
|
||||
```bash
|
||||
apt-get install -y tor
|
||||
cat >> /etc/tor/torrc << 'TOR'
|
||||
HiddenServiceDir /var/lib/tor/nntpchan/
|
||||
HiddenServicePort 80 127.0.0.1:80
|
||||
TOR
|
||||
systemctl restart tor
|
||||
cat /var/lib/tor/nntpchan/hostname
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What was fixed in this fork
|
||||
|
||||
- Fixed compilation for modern Go (1.23+)
|
||||
- Fixed old-style GOPATH builds in the Makefile
|
||||
- Added posts-per-day graph (overcock graph) to front page
|
||||
- Added board stats table (Posts this Hour, Posts Today, Total)
|
||||
- Fixed date offset bug in posts graph
|
||||
- Built and fixed the JavaScript bundle (livechan, banners, post reply, etc.)
|
||||
- Captcha only required for new threads, not replies
|
||||
- Admin posts highlighted with admin.png via pubkey CSS
|
||||
- Fixed pubkey not loading correctly in thread view
|
||||
- Fixed PostgreSQL schema permissions for Ubuntu 24.04
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
[Discord](https://discord.gg)
|
||||
|
||||
## Bugs and issues
|
||||
|
||||
Please report bugs on the [GitHub issue tracker](https://github.com/tomoko-dev9/nntpchan/issues).
|
||||
|
||||
## Clients
|
||||
|
||||
NNTP (confirmed working):
|
||||
* Thunderbird
|
||||
|
||||
Web:
|
||||
* [Yukko](https://github.com/faissaloo/Yukko) — ncurses-based nntpchan web UI reader
|
||||
|
||||
## History
|
||||
|
||||
* Started in mid 2013 on anonet
|
||||
|
||||

|
||||
|
||||
[source code for map generation](https://github.com/nilesr/nntpchan-mapper)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
* [Deavmi](https://deavmi.carteronline.net/) — Making the documentation beautiful.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
## TODO ##
|
||||
|
||||
* imrpove frontend templates
|
||||
* extra stylesheets
|
||||
* more alternative templates
|
||||
* javascript free mod panel
|
||||
* liveui
|
||||
* better mod panel
|
||||
* easier peering
|
||||
* improve command line mod tools
|
||||
* refactor srnd package
|
||||
* configurable thumbnail size [issue #40](https://github.com/majestrate/nntpchan/issues/40)
|
||||
* postgres password bug [issue #137](https://github.com/majestrate/nntpchan/issues/137)
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
root=$(readlink -e $(dirname $0))
|
||||
set -e
|
||||
if [ "x" == "x$root" ] ; then
|
||||
root=$PWD/${0##*}
|
||||
fi
|
||||
cd $root
|
||||
|
||||
if [ -z "$GOPATH" ]; then
|
||||
export GOPATH=$root/go
|
||||
mkdir -p $GOPATH
|
||||
fi
|
||||
|
||||
if [ ! -f $GOPATH/bin/minify ]; then
|
||||
echo "set up minifiy"
|
||||
go get -v github.com/tdewolff/minify/cmd/minify
|
||||
fi
|
||||
if [ ! -f $GOPATH/bin/gopherjs ]; then
|
||||
echo "set up gopherjs"
|
||||
go get -v -u github.com/gopherjs/gopherjs
|
||||
fi
|
||||
|
||||
# build cuckoo miner
|
||||
echo "Building cuckoo miner"
|
||||
go get -v -u github.com/ZiRo-/cuckgo/miner_js
|
||||
$GOPATH/bin/gopherjs -m -v build github.com/ZiRo-/cuckgo/miner_js
|
||||
mv ./miner_js.js ./contrib/static/miner-js.js
|
||||
rm ./miner_js.js.map
|
||||
|
||||
outfile=$PWD/contrib/static/nntpchan.js
|
||||
|
||||
lint() {
|
||||
if [ "x$(which jslint)" == "x" ] ; then
|
||||
# no jslint
|
||||
true
|
||||
else
|
||||
echo "jslint: $1"
|
||||
jslint --browser $1
|
||||
fi
|
||||
}
|
||||
|
||||
mini() {
|
||||
echo "minify $1"
|
||||
echo "" >> $2
|
||||
echo "/* local file: $1 */" >> $2
|
||||
$GOPATH/bin/minify --mime=text/javascript >> $2 < $1
|
||||
}
|
||||
|
||||
# do linting too
|
||||
if [ "x$1" == "xlint" ] ; then
|
||||
echo "linting..."
|
||||
for f in ./contrib/js/*.js ; do
|
||||
lint $f
|
||||
done
|
||||
fi
|
||||
|
||||
echo -e "//For source code and license information please check https://github.com/majestrate/nntpchan \n" > $outfile
|
||||
|
||||
if [ -e ./contrib/js/contrib/*.js ] ; then
|
||||
for f in ./contrib/js/contrib/*.js ; do
|
||||
mini $f $outfile
|
||||
done
|
||||
fi
|
||||
|
||||
mini ./contrib/js/main.js_ $outfile
|
||||
|
||||
# local js
|
||||
for f in ./contrib/js/*.js ; do
|
||||
mini $f $outfile
|
||||
done
|
||||
echo "ok"
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
root=$(readlink -e $(dirname $0))
|
||||
set -e
|
||||
if [ "x" == "x$root" ] ; then
|
||||
root=$PWD/${0##*}
|
||||
fi
|
||||
cd $root
|
||||
|
||||
tags=""
|
||||
|
||||
help_text="usage: $0 [--disable-redis]"
|
||||
|
||||
# check for help flags first
|
||||
for arg in $@ ; do
|
||||
case $arg in
|
||||
-h|--help)
|
||||
echo $help_text
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
rev="QmPAqM7anxdr1ngPmJz9J9AAxDLinDz2Eh9aAzLF9T7LNa"
|
||||
ipfs="no"
|
||||
rebuildjs="yes"
|
||||
_next=""
|
||||
# check for build flags
|
||||
for arg in $@ ; do
|
||||
case $arg in
|
||||
"--no-js")
|
||||
rebuildjs="no"
|
||||
;;
|
||||
"--ipfs")
|
||||
ipfs="yes"
|
||||
;;
|
||||
"--cuckoo")
|
||||
cuckoo="yes"
|
||||
;;
|
||||
"--disable-redis")
|
||||
tags="$tags -tags disable_redis"
|
||||
;;
|
||||
"--revision")
|
||||
_next="rev"
|
||||
;;
|
||||
"--revision=*")
|
||||
rev=$(echo $arg | cut -d'=' -f2)
|
||||
;;
|
||||
*)
|
||||
if [ "x$_next" == "xrev" ] ; then
|
||||
rev="$arg"
|
||||
fi
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "x$rev" == "x" ] ; then
|
||||
echo "revision not specified"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd $root
|
||||
if [ "x$rebuildjs" == "xyes" ] ; then
|
||||
echo "rebuilding generated js..."
|
||||
./build-js.sh
|
||||
fi
|
||||
unset GOPATH
|
||||
export GOPATH=$PWD/go
|
||||
mkdir -p $GOPATH
|
||||
|
||||
if [ "x$ipfs" == "xyes" ] ; then
|
||||
if [ ! -e $GOPATH/bin/gx ] ; then
|
||||
echo "obtaining gx"
|
||||
go get -u -v github.com/whyrusleeping/gx
|
||||
fi
|
||||
if [ ! -e $GOPATH/bin/gx-go ] ; then
|
||||
echo "obtaining gx-go"
|
||||
go get -u -v github.com/whyrusleeping/gx-go
|
||||
fi
|
||||
echo "building stable revision, this will take a bit. to speed this part up install and run ipfs locally"
|
||||
mkdir -p $GOPATH/src/gx/ipfs
|
||||
cd $GOPATH/src/gx/ipfs
|
||||
$GOPATH/bin/gx get $rev
|
||||
cd $root
|
||||
go get -d -v
|
||||
go build -v .
|
||||
mv nntpchan srndv2
|
||||
else
|
||||
go get -u -v github.com/majestrate/srndv2
|
||||
cp $GOPATH/bin/srndv2 $root
|
||||
fi
|
||||
|
||||
echo -e "Built\n"
|
||||
echo "Now configure NNTPChan with ./srndv2 setup"
|
||||
@@ -0,0 +1,14 @@
|
||||
TabWidth: 2
|
||||
UseTab: Never
|
||||
ColumnLimit: 120
|
||||
IndentWidth: 2
|
||||
Language: Cpp
|
||||
BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: true
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterStruct: true
|
||||
BeforeElse: true
|
||||
@@ -0,0 +1,6 @@
|
||||
*.o
|
||||
*.a
|
||||
nntpd
|
||||
tools/authtool
|
||||
tools/testtool
|
||||
.gdb_history
|
||||
@@ -0,0 +1,80 @@
|
||||
|
||||
REPO=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
NNTPCHAN_PATH = $(REPO)/libnntpchan
|
||||
|
||||
NNTPCHAN_SRC := $(wildcard $(NNTPCHAN_PATH)/*.cpp)
|
||||
NNTPCHAN_HDR := $(wildcard $(NNTPCHAN_PATH)/*.hpp)
|
||||
NNTPCHAN_OBJ := $(NNTPCHAN_SRC:.cpp=.o)
|
||||
|
||||
HEADERS_PATH=$(REPO)/include
|
||||
|
||||
TOOL_PATH := $(REPO)/tools
|
||||
|
||||
TOOL_SRC := $(wildcard $(TOOL_PATH)/*.cpp)
|
||||
TOOLS := $(TOOL_SRC:.cpp=)
|
||||
|
||||
SRCS = $(NNTPCHAN_SRC) $(TOOL_SRC)
|
||||
|
||||
OBJ := $(NNTPCHAN_OBJ)
|
||||
|
||||
TEST = $(REPO)/test
|
||||
|
||||
DAEMON_SRC = $(REPO)/daemon
|
||||
|
||||
LD_FLAGS ?=
|
||||
|
||||
INC_FLAGS = -I$(HEADERS_PATH)
|
||||
|
||||
ifeq ($(shell uname -s),FreeBSD)
|
||||
LD_FLAGS += $(shell dirname $(CXX))/../lib/libc++experimental.a
|
||||
INC_FLAGS += -I/usr/local/include
|
||||
LD_FLAGS += /usr/local/lib/libsodium.a
|
||||
else
|
||||
LD_FLAGS += -lstdc++fs
|
||||
INC_FLAGS += $(shell pkg-config --cflags libsodium)
|
||||
LD_FLAGS += $(shell pkg-config --libs --static libsodium)
|
||||
endif
|
||||
|
||||
REQUIRED_CXXFLAGS = -std=c++17 -Wall -Wextra -Werror -pedantic $(INC_FLAGS)
|
||||
|
||||
DEBUG = 1
|
||||
|
||||
ifeq ($(DEBUG),1)
|
||||
REQUIRED_CXXFLAGS += -g
|
||||
endif
|
||||
|
||||
CXXFLAGS += $(REQUIRED_CXXFLAGS)
|
||||
|
||||
NNTPCHAN_LIB = $(REPO)/libnntpchan.a
|
||||
|
||||
LIBS = $(NNTPCHAN_LIB)
|
||||
|
||||
EXE = $(REPO)/nntpd
|
||||
|
||||
|
||||
all: build
|
||||
|
||||
format:
|
||||
clang-format -i $(SRCS)
|
||||
|
||||
build: $(EXE) tools
|
||||
|
||||
$(NNTPCHAN_LIB): $(NNTPCHAN_OBJ)
|
||||
$(AR) -r $(NNTPCHAN_LIB) $(NNTPCHAN_OBJ)
|
||||
|
||||
$(EXE): $(LIBS)
|
||||
$(CXX) $(CXXFLAGS) $(DAEMON_SRC)/main.cpp $(LIBS) $(LD_FLAGS) -o $(EXE)
|
||||
|
||||
tools: $(TOOLS)
|
||||
|
||||
$(TOOLS): $(LIBS)
|
||||
$(CXX) $(CXXFLAGS) $@.cpp $(LIBS) $(LD_FLAGS) -o $@
|
||||
|
||||
build-test: $(LIBS)
|
||||
$(CXX) -o $(TEST) $(CXXFLAGS) test.cpp $(LIBS) $(LD_FLAGS)
|
||||
|
||||
test: build-test
|
||||
$(TEST)
|
||||
|
||||
clean:
|
||||
$(RM) $(OBJ) $(LIBS) $(EXE) $(TOOLS) $(TEST)
|
||||
@@ -0,0 +1,19 @@
|
||||
# nntpchan-daemon
|
||||
|
||||
C++ rewrite
|
||||
|
||||
requirements:
|
||||
|
||||
* C++17 compiler
|
||||
|
||||
* libsodium 1.x
|
||||
|
||||
* GNU Make
|
||||
|
||||
building on freebsd:
|
||||
|
||||
$ gmake
|
||||
|
||||
building on Linux:
|
||||
|
||||
$ make
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) <2015> <carriez.md@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INI_HPP
|
||||
#define INI_HPP
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace INI
|
||||
{
|
||||
|
||||
struct Level
|
||||
{
|
||||
Level() : parent(NULL), depth(0) {}
|
||||
Level(Level *p) : parent(p), depth(0) {}
|
||||
|
||||
typedef std::map<std::string, std::string> value_map_t;
|
||||
typedef std::map<std::string, Level> section_map_t;
|
||||
typedef std::list<value_map_t::const_iterator> values_t;
|
||||
typedef std::list<section_map_t::const_iterator> sections_t;
|
||||
value_map_t values;
|
||||
section_map_t sections;
|
||||
values_t ordered_values; // original order in the ini file
|
||||
sections_t ordered_sections;
|
||||
Level *parent;
|
||||
size_t depth;
|
||||
|
||||
const std::string &operator[](const std::string &name) { return values[name]; }
|
||||
Level &operator()(const std::string &name) { return sections[name]; }
|
||||
};
|
||||
|
||||
class Parser
|
||||
{
|
||||
public:
|
||||
Parser(const char *fn);
|
||||
Parser(std::istream &f) : f_(&f), ln_(0) { parse(top_); }
|
||||
Level &top() { return top_; }
|
||||
void dump(std::ostream &s) { dump(s, top(), ""); }
|
||||
|
||||
private:
|
||||
void dump(std::ostream &s, const Level &l, const std::string &sname);
|
||||
void parse(Level &l);
|
||||
void parseSLine(std::string &sname, size_t &depth);
|
||||
void err(const char *s);
|
||||
|
||||
private:
|
||||
Level top_;
|
||||
std::ifstream f0_;
|
||||
std::istream *f_;
|
||||
std::string line_;
|
||||
size_t ln_;
|
||||
};
|
||||
|
||||
inline void Parser::err(const char *s)
|
||||
{
|
||||
char buf[256];
|
||||
sprintf(buf, "%s on line #%ld", s, ln_);
|
||||
throw std::runtime_error(buf);
|
||||
}
|
||||
|
||||
inline std::string trim(const std::string &s)
|
||||
{
|
||||
char p[] = " \t\r\n";
|
||||
long sp = 0;
|
||||
long ep = s.length() - 1;
|
||||
for (; sp <= ep; ++sp)
|
||||
if (!strchr(p, s[sp]))
|
||||
break;
|
||||
for (; ep >= 0; --ep)
|
||||
if (!strchr(p, s[ep]))
|
||||
break;
|
||||
return s.substr(sp, ep - sp + 1);
|
||||
}
|
||||
|
||||
inline Parser::Parser(const char *fn) : f0_(fn), f_(&f0_), ln_(0)
|
||||
{
|
||||
if (!f0_)
|
||||
throw std::runtime_error(std::string("failed to open file: ") + fn);
|
||||
|
||||
parse(top_);
|
||||
}
|
||||
|
||||
inline void Parser::parseSLine(std::string &sname, size_t &depth)
|
||||
{
|
||||
depth = 0;
|
||||
for (; depth < line_.length(); ++depth)
|
||||
if (line_[depth] != '[')
|
||||
break;
|
||||
|
||||
sname = line_.substr(depth, line_.length() - 2 * depth);
|
||||
}
|
||||
|
||||
inline void Parser::parse(Level &l)
|
||||
{
|
||||
while (std::getline(*f_, line_))
|
||||
{
|
||||
++ln_;
|
||||
if (line_[0] == '#' || line_[0] == ';')
|
||||
continue;
|
||||
line_ = trim(line_);
|
||||
if (line_.empty())
|
||||
continue;
|
||||
if (line_[0] == '[')
|
||||
{
|
||||
size_t depth;
|
||||
std::string sname;
|
||||
parseSLine(sname, depth);
|
||||
Level *lp = NULL;
|
||||
Level *parent = &l;
|
||||
if (depth > l.depth + 1)
|
||||
err("section with wrong depth");
|
||||
if (l.depth == depth - 1)
|
||||
lp = &l.sections[sname];
|
||||
else
|
||||
{
|
||||
lp = l.parent;
|
||||
size_t n = l.depth - depth;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
lp = lp->parent;
|
||||
parent = lp;
|
||||
lp = &lp->sections[sname];
|
||||
}
|
||||
if (lp->depth != 0)
|
||||
err("duplicate section name on the same level");
|
||||
if (!lp->parent)
|
||||
{
|
||||
lp->depth = depth;
|
||||
lp->parent = parent;
|
||||
}
|
||||
parent->ordered_sections.push_back(parent->sections.find(sname));
|
||||
parse(*lp);
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t n = line_.find('=');
|
||||
if (n == std::string::npos)
|
||||
err("no '=' found");
|
||||
std::pair<Level::value_map_t::const_iterator, bool> res =
|
||||
l.values.insert(std::make_pair(trim(line_.substr(0, n)), trim(line_.substr(n + 1, line_.length() - n - 1))));
|
||||
if (!res.second)
|
||||
err("duplicated key found");
|
||||
l.ordered_values.push_back(res.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void Parser::dump(std::ostream &s, const Level &l, const std::string &sname)
|
||||
{
|
||||
if (!sname.empty())
|
||||
s << '\n';
|
||||
for (size_t i = 0; i < l.depth; ++i)
|
||||
s << '[';
|
||||
if (!sname.empty())
|
||||
s << sname;
|
||||
for (size_t i = 0; i < l.depth; ++i)
|
||||
s << ']';
|
||||
if (!sname.empty())
|
||||
s << std::endl;
|
||||
for (Level::values_t::const_iterator it = l.ordered_values.begin(); it != l.ordered_values.end(); ++it)
|
||||
s << (*it)->first << '=' << (*it)->second << std::endl;
|
||||
for (Level::sections_t::const_iterator it = l.ordered_sections.begin(); it != l.ordered_sections.end(); ++it)
|
||||
{
|
||||
assert((*it)->second.depth == l.depth + 1);
|
||||
dump(s, (*it)->second, (*it)->first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // INI_HPP
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "ini.hpp"
|
||||
|
||||
#include <nntpchan/crypto.hpp>
|
||||
#include <nntpchan/event.hpp>
|
||||
#include <nntpchan/exec_frontend.hpp>
|
||||
#include <nntpchan/nntp_server.hpp>
|
||||
#include <nntpchan/staticfile_frontend.hpp>
|
||||
#include <nntpchan/storage.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
int main(int argc, char *argv[], char * argenv[])
|
||||
{
|
||||
if (argc != 2)
|
||||
{
|
||||
std::cerr << "usage: " << argv[0] << " config.ini" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
nntpchan::Crypto crypto;
|
||||
|
||||
std::unique_ptr<nntpchan::ev::Loop> loop(nntpchan::NewMainLoop());
|
||||
|
||||
std::unique_ptr<nntpchan::NNTPServer> nntp = std::make_unique<nntpchan::NNTPServer>(loop.get());
|
||||
|
||||
std::string fname(argv[1]);
|
||||
|
||||
std::ifstream i(fname);
|
||||
|
||||
if (i.is_open())
|
||||
{
|
||||
INI::Parser conf(i);
|
||||
|
||||
std::vector<std::string> requiredSections = {"nntp", "articles"};
|
||||
|
||||
auto &level = conf.top();
|
||||
|
||||
for (const auto §ion : requiredSections)
|
||||
{
|
||||
if (level.sections.find(section) == level.sections.end())
|
||||
{
|
||||
std::cerr << "config file " << fname << " does not have required section: ";
|
||||
std::cerr << section << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
auto &storeconf = level.sections["articles"].values;
|
||||
|
||||
if (storeconf.find("store_path") == storeconf.end())
|
||||
{
|
||||
std::cerr << "storage section does not have 'store_path' value" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
nntp->SetStoragePath(storeconf["store_path"]);
|
||||
|
||||
auto &nntpconf = level.sections["nntp"].values;
|
||||
|
||||
if (nntpconf.find("bind") == nntpconf.end())
|
||||
{
|
||||
std::cerr << "nntp section does not have 'bind' value" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nntpconf.find("instance_name") == nntpconf.end())
|
||||
{
|
||||
std::cerr << "nntp section lacks 'instance_name' value" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
nntp->SetInstanceName(nntpconf["instance_name"]);
|
||||
|
||||
if (nntpconf.find("authdb") != nntpconf.end())
|
||||
{
|
||||
nntp->SetLoginDB(nntpconf["authdb"]);
|
||||
}
|
||||
|
||||
if (level.sections.find("frontend") != level.sections.end())
|
||||
{
|
||||
// frontend enabled
|
||||
auto &frontconf = level.sections["frontend"].values;
|
||||
if (frontconf.find("type") == frontconf.end())
|
||||
{
|
||||
std::cerr << "frontend section provided but 'type' value not provided" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
auto &ftype = frontconf["type"];
|
||||
if (ftype == "exec")
|
||||
{
|
||||
if (frontconf.find("exec") == frontconf.end())
|
||||
{
|
||||
std::cerr << "exec frontend specified but no 'exec' value provided" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
nntp->SetFrontend(new nntpchan::ExecFrontend(frontconf["exec"], argenv));
|
||||
}
|
||||
else if (ftype == "staticfile")
|
||||
{
|
||||
auto required = {"template_dir", "out_dir", "template_dialect", "max_pages"};
|
||||
for (const auto &opt : required)
|
||||
{
|
||||
if (frontconf.find(opt) == frontconf.end())
|
||||
{
|
||||
std::cerr << "staticfile frontend specified but no '" << opt << "' value provided" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
auto maxPages = std::stoi(frontconf["max_pages"]);
|
||||
if (maxPages <= 0)
|
||||
{
|
||||
std::cerr << "max_pages invalid value '" << frontconf["max_pages"] << "'" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
auto & dialect = frontconf["template_dialect"];
|
||||
auto templateEngine = nntpchan::CreateTemplateEngine(dialect);
|
||||
if(templateEngine == nullptr)
|
||||
{
|
||||
std::cerr << "invalid template dialect '" << dialect << "'" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
nntp->SetFrontend(new nntpchan::StaticFileFrontend(templateEngine, frontconf["template_dir"], frontconf["out_dir"], maxPages));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "unknown frontend type '" << ftype << "'" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "no frontend configured, running without generating markup" << std::endl;
|
||||
}
|
||||
|
||||
auto &a = nntpconf["bind"];
|
||||
|
||||
try
|
||||
{
|
||||
if(nntp->Bind(a))
|
||||
{
|
||||
std::cerr << "nntpd for " << nntp->InstanceName() << " bound to " << a << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "nntpd for " << nntp->InstanceName() << " failed to bind to " << a << ": "<< strerror(errno) << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} catch (std::exception &ex)
|
||||
{
|
||||
std::cerr << "failed to bind: " << ex.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
loop->Run();
|
||||
std::cerr << "Exiting" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "failed to open " << fname << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef NNTPCHAN_BASE64_HPP
|
||||
#define NNTPCHAN_BASE64_HPP
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
/** returns base64 encoded string */
|
||||
std::string B64Encode(const uint8_t *data, const std::size_t l);
|
||||
|
||||
/** @brief returns true if decode was successful */
|
||||
bool B64Decode(const std::string &data, std::vector<uint8_t> &out);
|
||||
|
||||
/** returns base32 encoded string */
|
||||
std::string B32Encode(const uint8_t *data, const std::size_t l);
|
||||
|
||||
/** @brief returns true if decode was successful */
|
||||
bool B32Decode(const std::string &data, std::vector<uint8_t> &out);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef NNTPCHAN_CRYPTO_HPP
|
||||
#define NNTPCHAN_CRYPTO_HPP
|
||||
|
||||
#include <array>
|
||||
#include <sodium/crypto_hash.h>
|
||||
#include <sodium/crypto_generichash.h>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
typedef std::array<uint8_t, crypto_hash_BYTES> SHA512Digest;
|
||||
|
||||
void SHA512(const uint8_t *d, std::size_t l, SHA512Digest &h);
|
||||
|
||||
typedef std::array<uint8_t, crypto_generichash_BYTES> Blake2BDigest;
|
||||
void Blake2B(const uint8_t *d, std::size_t l, Blake2BDigest & h);
|
||||
|
||||
std::string Blake2B_base32(const std::string & str);
|
||||
|
||||
|
||||
/** global crypto initializer */
|
||||
struct Crypto
|
||||
{
|
||||
Crypto();
|
||||
~Crypto();
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef NNTPCHAN_EVENT_HPP
|
||||
#define NNTPCHAN_EVENT_HPP
|
||||
|
||||
#include <unistd.h>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
namespace ev
|
||||
{
|
||||
struct io
|
||||
{
|
||||
int fd;
|
||||
|
||||
io(int f) : fd(f) {};
|
||||
virtual ~io() {};
|
||||
virtual bool readable() const { return true; };
|
||||
virtual int read(char * buf, size_t sz) = 0;
|
||||
virtual bool writeable() const { return true; };
|
||||
virtual int write(size_t avail) = 0;
|
||||
virtual bool keepalive() = 0;
|
||||
virtual void close()
|
||||
{
|
||||
if(fd!=-1)
|
||||
{
|
||||
::close(fd);
|
||||
}
|
||||
};
|
||||
virtual bool acceptable() const { return false; };
|
||||
virtual int accept() { return -1; };
|
||||
};
|
||||
|
||||
struct Loop
|
||||
{
|
||||
public:
|
||||
virtual ~Loop() {};
|
||||
|
||||
bool BindTCP(const sockaddr * addr, ev::io * handler);
|
||||
virtual bool TrackConn(ev::io * handler) = 0;
|
||||
virtual void UntrackConn(ev::io * handler) = 0;
|
||||
virtual void Run() = 0;
|
||||
bool SetNonBlocking(ev::io *handler);
|
||||
};
|
||||
}
|
||||
|
||||
ev::Loop * NewMainLoop();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef NNTPCHAN_EXEC_FRONTEND_HPP
|
||||
#define NNTPCHAN_EXEC_FRONTEND_HPP
|
||||
#include "frontend.hpp"
|
||||
#include <deque>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
class ExecFrontend : public Frontend
|
||||
{
|
||||
public:
|
||||
ExecFrontend(const std::string &exe, char * const* env);
|
||||
|
||||
~ExecFrontend();
|
||||
|
||||
void ProcessNewMessage(const fs::path &fpath);
|
||||
bool AcceptsNewsgroup(const std::string &newsgroup);
|
||||
bool AcceptsMessage(const std::string &msgid);
|
||||
|
||||
private:
|
||||
int Exec(std::deque<std::string> args);
|
||||
|
||||
private:
|
||||
char * const* m_Environ;
|
||||
std::string m_exec;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef NNTPCHAN_FILE_HANDLE_HPP
|
||||
#define NNTPCHAN_FILE_HANDLE_HPP
|
||||
|
||||
#include <experimental/filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
typedef std::unique_ptr<std::fstream> FileHandle_ptr;
|
||||
|
||||
enum FileMode
|
||||
{
|
||||
eRead,
|
||||
eWrite
|
||||
};
|
||||
|
||||
namespace fs = std::experimental::filesystem;
|
||||
|
||||
FileHandle_ptr OpenFile(const fs::path &fname, FileMode mode);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef NNTPCHAN_FRONTEND_HPP
|
||||
#define NNTPCHAN_FRONTEND_HPP
|
||||
#include <experimental/filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
namespace fs = std::experimental::filesystem;
|
||||
/** @brief nntpchan frontend ui interface */
|
||||
class Frontend
|
||||
{
|
||||
public:
|
||||
virtual ~Frontend() {};
|
||||
/** @brief process an inbound message stored at fpath that we have accepted. */
|
||||
virtual void ProcessNewMessage(const fs::path &fpath) = 0;
|
||||
|
||||
/** @brief return true if we take posts in a newsgroup */
|
||||
virtual bool AcceptsNewsgroup(const std::string &newsgroup) = 0;
|
||||
|
||||
/** @brief return true if we will accept a message given its message-id */
|
||||
virtual bool AcceptsMessage(const std::string &msgid) = 0;
|
||||
};
|
||||
|
||||
typedef std::unique_ptr<Frontend> Frontend_ptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
#ifndef NNTPCHAN_HTTP_HPP
|
||||
#define NNTPCHAN_HTTP_HPP
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
#ifndef NNTPCHAN_HTTP_CLIENT_HPP
|
||||
#define NNTPCHAN_HTTP_CLIENT_HPP
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
#ifndef NNTPCHAN_HTTP_SERVER_HPP
|
||||
#define NNTPCHAN_HTTP_SERVER_HPP
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef NNTPCHAN_IO_HANDLE_HPP
|
||||
#define NNTPCHAN_IO_HANDLE_HPP
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
typedef std::unique_ptr<std::iostream> IOHandle_ptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef NNTPCHAN_LINE_HPP
|
||||
#define NNTPCHAN_LINE_HPP
|
||||
#include "server.hpp"
|
||||
#include <stdint.h>
|
||||
#include <sstream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
/** @brief a buffered line reader */
|
||||
class LineReader
|
||||
{
|
||||
public:
|
||||
|
||||
/** @brief queue inbound data from connection */
|
||||
void Data(const char *data, ssize_t s);
|
||||
|
||||
protected:
|
||||
/** @brief handle a line from the client */
|
||||
virtual void HandleLine(const std::string line) = 0;
|
||||
|
||||
private:
|
||||
|
||||
std::stringstream m_line;
|
||||
std::string m_leftover;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef NNTPCHAN_MESSAGE_HPP
|
||||
#define NNTPCHAN_MESSAGE_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <nntpchan/model.hpp>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
struct MessageDB
|
||||
{
|
||||
using BoardPage = nntpchan::model::BoardPage;
|
||||
using Thread = nntpchan::model::Thread;
|
||||
virtual ~MessageDB() {};
|
||||
virtual bool LoadBoardPage(BoardPage &board, const std::string &newsgroup, uint32_t perpage, uint32_t page) const = 0;
|
||||
virtual bool FindThreadByHash(const std::string &hashhex, std::string &msgid) const = 0;
|
||||
virtual bool LoadThread(Thread &thread, const std::string &rootmsgid) const = 0;
|
||||
};
|
||||
|
||||
typedef std::unique_ptr<MessageDB> MessageDB_ptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef NNTPCHAN_MIME_HPP
|
||||
#define NNTPCHAN_MIME_HPP
|
||||
#include "file_handle.hpp"
|
||||
#include "io_handle.hpp"
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
typedef std::map<std::string, std::string> RawHeader;
|
||||
|
||||
bool ReadHeader(const FileHandle_ptr &f, RawHeader &h);
|
||||
|
||||
struct MimePart
|
||||
{
|
||||
virtual RawHeader &Header() = 0;
|
||||
virtual IOHandle_ptr OpenPart() = 0;
|
||||
};
|
||||
|
||||
typedef std::unique_ptr<MimePart> MimePart_ptr;
|
||||
|
||||
typedef std::function<bool(MimePart_ptr)> PartReader;
|
||||
|
||||
bool ReadParts(const FileHandle_ptr &f, PartReader r);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef NNTPCHAN_MODEL_HPP
|
||||
#define NNTPCHAN_MODEL_HPP
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <nntpchan/sanitize.hpp>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
namespace model
|
||||
{
|
||||
// MIME Header
|
||||
typedef std::map<std::string, std::vector<std::string>> PostHeader;
|
||||
// text post contents
|
||||
typedef std::string PostBody;
|
||||
// single file attachment, (orig_filename, hexdigest, thumb_filename)
|
||||
typedef std::tuple<std::string, std::string, std::string> PostAttachment;
|
||||
// all attachments on a post
|
||||
typedef std::vector<PostAttachment> Attachments;
|
||||
// a post (header, Post Text, Attachments)
|
||||
typedef std::tuple<PostHeader, PostBody, Attachments> Post;
|
||||
// a thread (many posts in post order)
|
||||
typedef std::vector<Post> Thread;
|
||||
// a board page is many threads in bump order
|
||||
struct BoardPage
|
||||
{
|
||||
std::vector<Thread> threads = {};
|
||||
std::string name = "";
|
||||
uint32_t pageno = 0;
|
||||
};
|
||||
|
||||
|
||||
static inline const std::string &GetFilename(const PostAttachment &att) { return std::get<0>(att); }
|
||||
|
||||
static inline const std::string &GetHexDigest(const PostAttachment &att) { return std::get<1>(att); }
|
||||
|
||||
static inline const std::string &GetThumbnail(const PostAttachment &att) { return std::get<2>(att); }
|
||||
|
||||
static inline const PostHeader &GetHeader(const Post &post) { return std::get<0>(post); }
|
||||
|
||||
static inline const PostBody &GetBody(const Post &post) { return std::get<1>(post); }
|
||||
|
||||
static inline const Attachments &GetAttachments(const Post &post) { return std::get<2>(post); }
|
||||
|
||||
static inline const std::string &HeaderIFind(const PostHeader &header, const std::string &val,
|
||||
const std::string &fallback)
|
||||
{
|
||||
std::string ival = ToLower(val);
|
||||
auto itr = std::find_if(header.begin(), header.end(),
|
||||
[ival](const auto &item) -> bool { return ToLower(item.first) == ival; });
|
||||
if (itr == std::end(header))
|
||||
return fallback;
|
||||
else
|
||||
return itr->second[0];
|
||||
}
|
||||
|
||||
using Model = std::variant<Thread, BoardPage>;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef NNTPCHAN_NET_HPP
|
||||
#define NNTPCHAN_NET_HPP
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <string>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
struct NetAddr
|
||||
{
|
||||
NetAddr();
|
||||
|
||||
sockaddr_in6 addr;
|
||||
operator sockaddr *() { return (sockaddr *)&addr; }
|
||||
operator const sockaddr *() const { return (sockaddr *)&addr; }
|
||||
std::string to_string();
|
||||
};
|
||||
|
||||
NetAddr ParseAddr(const std::string &addr);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef NNTPCHAN_NNTP_AUTH_HPP
|
||||
#define NNTPCHAN_NNTP_AUTH_HPP
|
||||
#include "line.hpp"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
/** @brief nntp credential db interface */
|
||||
class NNTPCredentialDB
|
||||
{
|
||||
public:
|
||||
/** @brief open connection to database, return false on error otherwise return true */
|
||||
virtual bool Open() = 0;
|
||||
/** @brief close connection to database */
|
||||
virtual void Close() = 0;
|
||||
/** @brief return true if username password combo is correct */
|
||||
virtual bool CheckLogin(const std::string &user, const std::string &passwd) = 0;
|
||||
virtual ~NNTPCredentialDB() {}
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<NNTPCredentialDB> CredDB_ptr;
|
||||
|
||||
/** @brief nntp credential db using hashed+salted passwords */
|
||||
class HashedCredDB : public NNTPCredentialDB, public LineReader
|
||||
{
|
||||
public:
|
||||
HashedCredDB();
|
||||
bool CheckLogin(const std::string &user, const std::string &passwd);
|
||||
|
||||
protected:
|
||||
void SetStream(std::istream *i);
|
||||
|
||||
std::string Hash(const std::string &data, const std::string &salt);
|
||||
void HandleLine(const std::string line);
|
||||
|
||||
private:
|
||||
bool ProcessLine(const std::string &line);
|
||||
|
||||
std::mutex m_access;
|
||||
std::string m_user, m_passwd;
|
||||
bool m_found;
|
||||
/** return true if we have a line that matches this username / password combo */
|
||||
std::istream *m_instream;
|
||||
};
|
||||
|
||||
class HashedFileDB : public HashedCredDB
|
||||
{
|
||||
public:
|
||||
HashedFileDB(const std::string &fname);
|
||||
~HashedFileDB();
|
||||
bool Open();
|
||||
void Close();
|
||||
|
||||
private:
|
||||
std::string m_fname;
|
||||
std::ifstream f;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef NNTPCHAN_NNTP_HANDLER_HPP
|
||||
#define NNTPCHAN_NNTP_HANDLER_HPP
|
||||
#include "line.hpp"
|
||||
#include "nntp_auth.hpp"
|
||||
#include "storage.hpp"
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
class NNTPServerHandler : public LineReader, public IConnHandler
|
||||
{
|
||||
public:
|
||||
NNTPServerHandler(fs::path storage);
|
||||
~NNTPServerHandler();
|
||||
|
||||
virtual bool ShouldClose();
|
||||
|
||||
void SetAuth(CredDB_ptr creds);
|
||||
|
||||
virtual void OnData(const char *, ssize_t);
|
||||
|
||||
void Greet();
|
||||
|
||||
protected:
|
||||
void HandleLine(const std::string line);
|
||||
void HandleCommand(const std::deque<std::string> &command);
|
||||
|
||||
private:
|
||||
enum State
|
||||
{
|
||||
eStateReadCommand,
|
||||
eStateStoreArticle,
|
||||
eStateQuit
|
||||
};
|
||||
|
||||
private:
|
||||
void EnterState(State st);
|
||||
|
||||
void ArticleObtained();
|
||||
|
||||
// handle quit command, this queues a reply
|
||||
void Quit();
|
||||
|
||||
// switch nntp modes, this queues a reply
|
||||
void SwitchMode(const std::string &mode);
|
||||
|
||||
bool PostingAllowed();
|
||||
|
||||
private:
|
||||
std::string m_articleName;
|
||||
FileHandle_ptr m_article;
|
||||
CredDB_ptr m_auth;
|
||||
ArticleStorage m_store;
|
||||
std::string m_mode;
|
||||
bool m_authed;
|
||||
State m_state;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef NNTPCHAN_NNTP_SERVER_HPP
|
||||
#define NNTPCHAN_NNTP_SERVER_HPP
|
||||
#include "frontend.hpp"
|
||||
#include "server.hpp"
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
class NNTPServer : public Server
|
||||
{
|
||||
public:
|
||||
NNTPServer(ev::Loop * loop);
|
||||
|
||||
virtual ~NNTPServer();
|
||||
|
||||
void SetStoragePath(const std::string &path);
|
||||
|
||||
void SetLoginDB(const std::string path);
|
||||
|
||||
void SetInstanceName(const std::string &name);
|
||||
|
||||
std::string InstanceName() const;
|
||||
|
||||
virtual IServerConn *CreateConn(int fd);
|
||||
|
||||
virtual void OnAcceptError(int status);
|
||||
|
||||
void SetFrontend(Frontend *f);
|
||||
|
||||
private:
|
||||
std::string m_logindbpath;
|
||||
std::string m_storagePath;
|
||||
std::string m_servername;
|
||||
|
||||
Frontend_ptr m_frontend;
|
||||
};
|
||||
|
||||
class NNTPServerConn : public IServerConn
|
||||
{
|
||||
public:
|
||||
NNTPServerConn(int fd, Server *parent, IConnHandler *h) : IServerConn(fd, parent, h) {}
|
||||
|
||||
virtual bool IsTimedOut() { return false; };
|
||||
|
||||
virtual void Greet();
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef NNTPCHAN_SANITIZE_HPP
|
||||
#define NNTPCHAN_SANITIZE_HPP
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
std::string NNTPSanitizeLine(const std::string &str);
|
||||
std::string ToLower(const std::string &str);
|
||||
std::string StripWhitespaces(const std::string &str);
|
||||
bool IsValidMessageID(const std::string &msgid);
|
||||
bool IsValidNewsgroup(const std::string &group);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,96 @@
|
||||
#ifndef NNTPCHAN_SERVER_HPP
|
||||
#define NNTPCHAN_SERVER_HPP
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <nntpchan/event.hpp>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
class Server;
|
||||
|
||||
struct IConnHandler
|
||||
{
|
||||
|
||||
virtual ~IConnHandler(){};
|
||||
|
||||
/** got inbound data */
|
||||
virtual void OnData(const char *data, ssize_t s) = 0;
|
||||
|
||||
/** get next line of data to send */
|
||||
std::string GetNextLine();
|
||||
|
||||
/** return true if we have a line to send */
|
||||
bool HasNextLine();
|
||||
|
||||
/** return true if we should close this connection otherwise return false */
|
||||
virtual bool ShouldClose() = 0;
|
||||
|
||||
/** queue a data send */
|
||||
void QueueLine(const std::string &line);
|
||||
|
||||
virtual void Greet() = 0;
|
||||
|
||||
private:
|
||||
std::deque<std::string> m_sendlines;
|
||||
};
|
||||
|
||||
/** server connection handler interface */
|
||||
struct IServerConn : public ev::io
|
||||
{
|
||||
IServerConn(int fd, Server *parent, IConnHandler *h);
|
||||
virtual ~IServerConn();
|
||||
virtual int read(char * buf, size_t sz);
|
||||
virtual int write(size_t avail);
|
||||
virtual void close();
|
||||
virtual void Greet() = 0;
|
||||
virtual bool IsTimedOut() = 0;
|
||||
virtual bool keepalive() ;
|
||||
Server *Parent() { return m_parent; };
|
||||
IConnHandler *GetHandler() { return m_handler; };
|
||||
|
||||
private:
|
||||
Server *m_parent;
|
||||
IConnHandler *m_handler;
|
||||
std::string m_writeLeftover;
|
||||
};
|
||||
|
||||
class Server : public ev::io
|
||||
{
|
||||
public:
|
||||
Server(ev::Loop * loop);
|
||||
virtual ~Server() {};
|
||||
|
||||
virtual bool acceptable() const { return true; };
|
||||
virtual void close();
|
||||
virtual bool readable() const { return false; };
|
||||
virtual int read(char *,size_t) { return -1; };
|
||||
virtual bool writeable() const { return false; };
|
||||
virtual int write(size_t) {return -1; };
|
||||
virtual int accept();
|
||||
virtual bool keepalive() { return true; };
|
||||
|
||||
|
||||
/** create connection handler from open stream */
|
||||
virtual IServerConn *CreateConn(int fd) = 0;
|
||||
/** bind to address */
|
||||
bool Bind(const std::string &addr);
|
||||
|
||||
typedef std::function<void(IServerConn *)> ConnVisitor;
|
||||
|
||||
/** visit all open connections */
|
||||
void VisitConns(ConnVisitor v);
|
||||
|
||||
/** remove connection from server, called after proper close */
|
||||
void RemoveConn(IServerConn *conn);
|
||||
|
||||
private:
|
||||
|
||||
void OnAccept(int fd);
|
||||
ev::Loop * m_Loop;
|
||||
std::deque<IServerConn *> m_conns;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef NNTPCHAN_SHA1_HPP
|
||||
#define NNTPCHAN_SHA1_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
std::string sha1_hex(const std::string &data);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef NNTPCHAN_STATICFILE_FRONTEND_HPP
|
||||
#define NNTPCHAN_STATICFILE_FRONTEND_HPP
|
||||
#include "frontend.hpp"
|
||||
#include "message.hpp"
|
||||
#include "model.hpp"
|
||||
#include "template_engine.hpp"
|
||||
#include <experimental/filesystem>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
namespace fs = std::experimental::filesystem;
|
||||
|
||||
class StaticFileFrontend : public Frontend
|
||||
{
|
||||
public:
|
||||
StaticFileFrontend(TemplateEngine *tmpl, const std::string &templateDir, const std::string &outDir, uint32_t pages);
|
||||
|
||||
virtual ~StaticFileFrontend();
|
||||
|
||||
virtual void ProcessNewMessage(const fs::path &fpath);
|
||||
virtual bool AcceptsNewsgroup(const std::string &newsgroup);
|
||||
virtual bool AcceptsMessage(const std::string &msgid);
|
||||
|
||||
private:
|
||||
MessageDB_ptr m_MessageDB;
|
||||
TemplateEngine_ptr m_TemplateEngine;
|
||||
fs::path m_TemplateDir;
|
||||
fs::path m_OutDir;
|
||||
uint32_t m_Pages;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef NNTPCHAN_STORAGE_HPP
|
||||
#define NNTPCHAN_STORAGE_HPP
|
||||
|
||||
#include "file_handle.hpp"
|
||||
#include "message.hpp"
|
||||
#include <experimental/filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
namespace fs = std::experimental::filesystem;
|
||||
|
||||
class ArticleStorage : public MessageDB
|
||||
{
|
||||
public:
|
||||
ArticleStorage(const fs::path &fpath);
|
||||
~ArticleStorage();
|
||||
|
||||
FileHandle_ptr OpenWrite(const std::string &msgid) const;
|
||||
FileHandle_ptr OpenRead(const std::string &msgid) const;
|
||||
|
||||
/**
|
||||
return true if we should accept a new message give its message id
|
||||
*/
|
||||
bool Accept(const std::string &msgid) const;
|
||||
|
||||
bool LoadBoardPage(BoardPage &board, const std::string &newsgroup, uint32_t perpage, uint32_t page) const;
|
||||
bool FindThreadByHash(const std::string &hashhex, std::string &msgid) const;
|
||||
bool LoadThread(Thread &thread, const std::string &rootmsgid) const;
|
||||
|
||||
/** ensure symlinks are formed for this article by message id */
|
||||
void EnsureSymlinks(const std::string &msgid) const;
|
||||
|
||||
private:
|
||||
void SetPath(const fs::path &fpath);
|
||||
|
||||
fs::path MessagePath(const std::string &msgid) const;
|
||||
|
||||
bool init_skiplist(const std::string &subdir) const;
|
||||
|
||||
fs::path skiplist_root(const std::string &name) const;
|
||||
fs::path skiplist_dir(const fs::path & root, const std::string & name) const;
|
||||
|
||||
fs::path basedir;
|
||||
};
|
||||
|
||||
typedef std::unique_ptr<ArticleStorage> ArticleStorage_ptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef NNTPCHAN_TEMPLATE_ENGINE_HPP
|
||||
#define NNTPCHAN_TEMPLATE_ENGINE_HPP
|
||||
#include "file_handle.hpp"
|
||||
#include "model.hpp"
|
||||
#include <any>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
struct TemplateEngine
|
||||
{
|
||||
virtual ~TemplateEngine() {};
|
||||
virtual bool WriteBoardPage(const nntpchan::model::BoardPage & page, const FileHandle_ptr &out) = 0;
|
||||
virtual bool WriteThreadPage(const nntpchan::model::Thread & thread, const FileHandle_ptr &out) = 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
typedef std::unique_ptr<TemplateEngine> TemplateEngine_ptr;
|
||||
|
||||
TemplateEngine * CreateTemplateEngine(const std::string &dialect);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,334 @@
|
||||
#include <nntpchan/base64.hpp>
|
||||
|
||||
// taken from i2pd
|
||||
namespace i2p
|
||||
{
|
||||
namespace data
|
||||
{
|
||||
|
||||
static const char T32[32] = {
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
|
||||
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '2', '3', '4', '5', '6', '7',
|
||||
};
|
||||
|
||||
const char *GetBase32SubstitutionTable() { return T32; }
|
||||
|
||||
static void iT64Build(void);
|
||||
|
||||
/*
|
||||
*
|
||||
* BASE64 Substitution Table
|
||||
* -------------------------
|
||||
*
|
||||
* Direct Substitution Table
|
||||
*/
|
||||
|
||||
static const char T64[64] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '~'};
|
||||
|
||||
const char *GetBase64SubstitutionTable() { return T64; }
|
||||
|
||||
/*
|
||||
* Reverse Substitution Table (built in run time)
|
||||
*/
|
||||
|
||||
static char iT64[256];
|
||||
static int isFirstTime = 1;
|
||||
|
||||
/*
|
||||
* Padding
|
||||
*/
|
||||
|
||||
static char P64 = '=';
|
||||
|
||||
/*
|
||||
*
|
||||
* ByteStreamToBase64
|
||||
* ------------------
|
||||
*
|
||||
* Converts binary encoded data to BASE64 format.
|
||||
*
|
||||
*/
|
||||
|
||||
size_t /* Number of bytes in the encoded buffer */
|
||||
ByteStreamToBase64(const uint8_t *InBuffer, /* Input buffer, binary data */
|
||||
size_t InCount, /* Number of bytes in the input buffer */
|
||||
char *OutBuffer, /* output buffer */
|
||||
size_t len /* length of output buffer */
|
||||
)
|
||||
|
||||
{
|
||||
unsigned char *ps;
|
||||
unsigned char *pd;
|
||||
unsigned char acc_1;
|
||||
unsigned char acc_2;
|
||||
int i;
|
||||
int n;
|
||||
int m;
|
||||
size_t outCount;
|
||||
|
||||
ps = (unsigned char *)InBuffer;
|
||||
n = InCount / 3;
|
||||
m = InCount % 3;
|
||||
if (!m)
|
||||
outCount = 4 * n;
|
||||
else
|
||||
outCount = 4 * (n + 1);
|
||||
if (outCount > len)
|
||||
return 0;
|
||||
pd = (unsigned char *)OutBuffer;
|
||||
for (i = 0; i < n; i++)
|
||||
{
|
||||
acc_1 = *ps++;
|
||||
acc_2 = (acc_1 << 4) & 0x30;
|
||||
acc_1 >>= 2; /* base64 digit #1 */
|
||||
*pd++ = T64[acc_1];
|
||||
acc_1 = *ps++;
|
||||
acc_2 |= acc_1 >> 4; /* base64 digit #2 */
|
||||
*pd++ = T64[acc_2];
|
||||
acc_1 &= 0x0f;
|
||||
acc_1 <<= 2;
|
||||
acc_2 = *ps++;
|
||||
acc_1 |= acc_2 >> 6; /* base64 digit #3 */
|
||||
*pd++ = T64[acc_1];
|
||||
acc_2 &= 0x3f; /* base64 digit #4 */
|
||||
*pd++ = T64[acc_2];
|
||||
}
|
||||
if (m == 1)
|
||||
{
|
||||
acc_1 = *ps++;
|
||||
acc_2 = (acc_1 << 4) & 0x3f; /* base64 digit #2 */
|
||||
acc_1 >>= 2; /* base64 digit #1 */
|
||||
*pd++ = T64[acc_1];
|
||||
*pd++ = T64[acc_2];
|
||||
*pd++ = P64;
|
||||
*pd++ = P64;
|
||||
}
|
||||
else if (m == 2)
|
||||
{
|
||||
acc_1 = *ps++;
|
||||
acc_2 = (acc_1 << 4) & 0x3f;
|
||||
acc_1 >>= 2; /* base64 digit #1 */
|
||||
*pd++ = T64[acc_1];
|
||||
acc_1 = *ps++;
|
||||
acc_2 |= acc_1 >> 4; /* base64 digit #2 */
|
||||
*pd++ = T64[acc_2];
|
||||
acc_1 &= 0x0f;
|
||||
acc_1 <<= 2; /* base64 digit #3 */
|
||||
*pd++ = T64[acc_1];
|
||||
*pd++ = P64;
|
||||
}
|
||||
|
||||
return outCount;
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* Base64ToByteStream
|
||||
* ------------------
|
||||
*
|
||||
* Converts BASE64 encoded data to binary format. If input buffer is
|
||||
* not properly padded, buffer of negative length is returned
|
||||
*
|
||||
*/
|
||||
|
||||
size_t /* Number of output bytes */
|
||||
Base64ToByteStream(const char *InBuffer, /* BASE64 encoded buffer */
|
||||
size_t InCount, /* Number of input bytes */
|
||||
uint8_t *OutBuffer, /* output buffer length */
|
||||
size_t len /* length of output buffer */
|
||||
)
|
||||
{
|
||||
unsigned char *ps;
|
||||
unsigned char *pd;
|
||||
unsigned char acc_1;
|
||||
unsigned char acc_2;
|
||||
int i;
|
||||
int n;
|
||||
int m;
|
||||
size_t outCount;
|
||||
|
||||
if (isFirstTime)
|
||||
iT64Build();
|
||||
n = InCount / 4;
|
||||
m = InCount % 4;
|
||||
if (InCount && !m)
|
||||
outCount = 3 * n;
|
||||
else
|
||||
{
|
||||
outCount = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ps = (unsigned char *)(InBuffer + InCount - 1);
|
||||
while (*ps-- == P64)
|
||||
outCount--;
|
||||
ps = (unsigned char *)InBuffer;
|
||||
|
||||
if (outCount > len)
|
||||
return -1;
|
||||
pd = OutBuffer;
|
||||
auto endOfOutBuffer = OutBuffer + outCount;
|
||||
for (i = 0; i < n; i++)
|
||||
{
|
||||
acc_1 = iT64[*ps++];
|
||||
acc_2 = iT64[*ps++];
|
||||
acc_1 <<= 2;
|
||||
acc_1 |= acc_2 >> 4;
|
||||
*pd++ = acc_1;
|
||||
if (pd >= endOfOutBuffer)
|
||||
break;
|
||||
|
||||
acc_2 <<= 4;
|
||||
acc_1 = iT64[*ps++];
|
||||
acc_2 |= acc_1 >> 2;
|
||||
*pd++ = acc_2;
|
||||
if (pd >= endOfOutBuffer)
|
||||
break;
|
||||
|
||||
acc_2 = iT64[*ps++];
|
||||
acc_2 |= acc_1 << 6;
|
||||
*pd++ = acc_2;
|
||||
}
|
||||
|
||||
return outCount;
|
||||
}
|
||||
|
||||
size_t Base64EncodingBufferSize(const size_t input_size)
|
||||
{
|
||||
auto d = div(input_size, 3);
|
||||
if (d.rem)
|
||||
d.quot++;
|
||||
return 4 * d.quot;
|
||||
}
|
||||
|
||||
size_t Base32EncodingBufferSize(const size_t input_size)
|
||||
{
|
||||
auto d = div(input_size, 5);
|
||||
if (d.rem)
|
||||
d.quot++;
|
||||
return 8 * d.quot;
|
||||
}
|
||||
/*
|
||||
*
|
||||
* iT64
|
||||
* ----
|
||||
* Reverse table builder. P64 character is replaced with 0
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
static void iT64Build()
|
||||
{
|
||||
int i;
|
||||
isFirstTime = 0;
|
||||
for (i = 0; i < 256; i++)
|
||||
iT64[i] = -1;
|
||||
for (i = 0; i < 64; i++)
|
||||
iT64[(int)T64[i]] = i;
|
||||
iT64[(int)P64] = 0;
|
||||
}
|
||||
|
||||
size_t Base32ToByteStream(const char *inBuf, size_t len, uint8_t *outBuf, size_t outLen)
|
||||
{
|
||||
int tmp = 0, bits = 0;
|
||||
size_t ret = 0;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
char ch = inBuf[i];
|
||||
if (ch >= '2' && ch <= '7') // digit
|
||||
ch = (ch - '2') + 26; // 26 means a-z
|
||||
else if (ch >= 'a' && ch <= 'z')
|
||||
ch = ch - 'a'; // a = 0
|
||||
else
|
||||
return 0; // unexpected character
|
||||
|
||||
tmp |= ch;
|
||||
bits += 5;
|
||||
if (bits >= 8)
|
||||
{
|
||||
if (ret >= outLen)
|
||||
return ret;
|
||||
outBuf[ret] = tmp >> (bits - 8);
|
||||
bits -= 8;
|
||||
ret++;
|
||||
}
|
||||
tmp <<= 5;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t ByteStreamToBase32(const uint8_t *inBuf, size_t len, char *outBuf, size_t outLen)
|
||||
{
|
||||
size_t ret = 0, pos = 1;
|
||||
int bits = 8, tmp = inBuf[0];
|
||||
while (ret < outLen && (bits > 0 || pos < len))
|
||||
{
|
||||
if (bits < 5)
|
||||
{
|
||||
if (pos < len)
|
||||
{
|
||||
tmp <<= 8;
|
||||
tmp |= inBuf[pos] & 0xFF;
|
||||
pos++;
|
||||
bits += 8;
|
||||
}
|
||||
else // last byte
|
||||
{
|
||||
tmp <<= (5 - bits);
|
||||
bits = 5;
|
||||
}
|
||||
}
|
||||
|
||||
bits -= 5;
|
||||
int ind = (tmp >> bits) & 0x1F;
|
||||
outBuf[ret] = (ind < 26) ? (ind + 'a') : ((ind - 26) + '2');
|
||||
ret++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
std::string B64Encode(const uint8_t *data, const std::size_t l)
|
||||
{
|
||||
std::string out;
|
||||
out.resize(i2p::data::Base64EncodingBufferSize(l));
|
||||
i2p::data::ByteStreamToBase64(data, l, &out[0], out.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
bool B64Decode(const std::string &data, std::vector<uint8_t> &out)
|
||||
{
|
||||
out.resize(data.size());
|
||||
if (i2p::data::Base64ToByteStream(data.c_str(), data.size(), &out[0], out.size()))
|
||||
{
|
||||
out.shrink_to_fit();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string B32Encode(const uint8_t *data, const std::size_t l)
|
||||
{
|
||||
std::string out;
|
||||
out.resize(i2p::data::Base32EncodingBufferSize(l));
|
||||
i2p::data::ByteStreamToBase32(data, l, &out[0], out.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
bool B32Decode(const std::string &data, std::vector<uint8_t> &out)
|
||||
{
|
||||
out.resize(data.size());
|
||||
if (i2p::data::Base32ToByteStream(data.c_str(), data.size(), &out[0], out.size()))
|
||||
{
|
||||
out.shrink_to_fit();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include <cassert>
|
||||
#include <nntpchan/base64.hpp>
|
||||
#include <nntpchan/crypto.hpp>
|
||||
#include <sodium.h>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
void SHA512(const uint8_t *d, const std::size_t l, SHA512Digest &h) { crypto_hash(h.data(), d, l); }
|
||||
|
||||
void Blake2B(const uint8_t *d, std::size_t l, Blake2BDigest &h)
|
||||
{
|
||||
crypto_generichash(h.data(), h.size(), d, l, nullptr, 0);
|
||||
}
|
||||
|
||||
std::string Blake2B_base32(const std::string &str)
|
||||
{
|
||||
Blake2BDigest d;
|
||||
Blake2B(reinterpret_cast<const uint8_t *>(str.c_str()), str.size(), d);
|
||||
return B32Encode(d.data(), d.size());
|
||||
}
|
||||
|
||||
Crypto::Crypto() { assert(sodium_init() == 0); }
|
||||
|
||||
Crypto::~Crypto() {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
#ifndef NNTPCHAN_CRYPTO_OLD_HPP
|
||||
#define NNTPCHAN_CRYPTO_OLD_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
extern "C" {
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t state[5];
|
||||
uint32_t count[2];
|
||||
unsigned char buffer[64];
|
||||
} SHA1_CTX;
|
||||
|
||||
void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]);
|
||||
|
||||
void SHA1Init(SHA1_CTX *context);
|
||||
|
||||
void SHA1Update(SHA1_CTX *context, const unsigned char *data, uint32_t len);
|
||||
|
||||
void SHA1Final(unsigned char digest[20], SHA1_CTX *context);
|
||||
|
||||
void sha1(uint8_t *hash_out, const uint8_t *str, size_t len);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,183 @@
|
||||
#include <cassert>
|
||||
#include <nntpchan/event.hpp>
|
||||
#include <sys/epoll.h>
|
||||
#include <unistd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/un.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/signalfd.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
namespace ev
|
||||
{
|
||||
template<size_t bufsz>
|
||||
struct EpollLoop : public Loop
|
||||
{
|
||||
size_t conns;
|
||||
int epollfd;
|
||||
char readbuf[bufsz];
|
||||
EpollLoop() : conns(0), epollfd(epoll_create1(EPOLL_CLOEXEC))
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~EpollLoop()
|
||||
{
|
||||
::close(epollfd);
|
||||
}
|
||||
|
||||
|
||||
|
||||
virtual bool TrackConn(ev::io * handler)
|
||||
{
|
||||
epoll_event ev;
|
||||
ev.data.ptr = handler;
|
||||
ev.events = EPOLLET;
|
||||
if(handler->readable() || handler->acceptable())
|
||||
{
|
||||
ev.events |= EPOLLIN;
|
||||
}
|
||||
if(handler->writeable())
|
||||
{
|
||||
ev.events |= EPOLLOUT;
|
||||
}
|
||||
if ( epoll_ctl(epollfd, EPOLL_CTL_ADD, handler->fd, &ev) == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
++conns;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void UntrackConn(ev::io * handler)
|
||||
{
|
||||
if(epoll_ctl(epollfd, EPOLL_CTL_DEL, handler->fd, nullptr) != -1)
|
||||
--conns;
|
||||
}
|
||||
|
||||
|
||||
virtual void Run()
|
||||
{
|
||||
epoll_event evs[512];
|
||||
epoll_event * ev;
|
||||
ev::io * handler;
|
||||
int res = -1;
|
||||
int idx ;
|
||||
|
||||
sigset_t mask;
|
||||
|
||||
sigemptyset(&mask);
|
||||
sigaddset(&mask, SIGWINCH);
|
||||
|
||||
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
|
||||
epoll_event sig_ev;
|
||||
sig_ev.data.fd = sfd;
|
||||
sig_ev.events = EPOLLIN;
|
||||
epoll_ctl(epollfd, EPOLL_CTL_ADD, sfd, &sig_ev);
|
||||
do
|
||||
{
|
||||
res = epoll_wait(epollfd, evs, 512, -1);
|
||||
idx = 0;
|
||||
while(idx < res)
|
||||
{
|
||||
errno = 0;
|
||||
ev = &evs[idx++];
|
||||
if(ev->data.fd == sfd)
|
||||
{
|
||||
read(sfd, readbuf, sizeof(readbuf));
|
||||
continue;
|
||||
}
|
||||
|
||||
handler = static_cast<ev::io *>(ev->data.ptr);
|
||||
|
||||
if(ev->events & EPOLLERR || ev->events & EPOLLHUP)
|
||||
{
|
||||
handler->close();
|
||||
delete handler;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handler->acceptable())
|
||||
{
|
||||
int acceptfd;
|
||||
bool errored = false;
|
||||
while(true)
|
||||
{
|
||||
acceptfd = handler->accept();
|
||||
if(acceptfd == -1)
|
||||
{
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
{
|
||||
break;
|
||||
}
|
||||
perror("accept()");
|
||||
errored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(errored)
|
||||
{
|
||||
handler->close();
|
||||
delete handler;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(ev->events & EPOLLIN && handler->readable())
|
||||
{
|
||||
bool errored = false;
|
||||
while(true)
|
||||
{
|
||||
int readed = handler->read(readbuf, sizeof(readbuf));
|
||||
if(readed == -1)
|
||||
{
|
||||
if(errno != EAGAIN)
|
||||
{
|
||||
perror("read()");
|
||||
handler->close();
|
||||
delete handler;
|
||||
errored = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (readed == 0)
|
||||
{
|
||||
handler->close();
|
||||
delete handler;
|
||||
errored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(errored) continue;
|
||||
}
|
||||
if(ev->events & EPOLLOUT && handler->writeable())
|
||||
{
|
||||
int written = handler->write(1024);
|
||||
if(written < 0)
|
||||
{
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
{
|
||||
// blocking
|
||||
}
|
||||
else
|
||||
{
|
||||
perror("write()");
|
||||
handler->close();
|
||||
delete handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!handler->keepalive())
|
||||
{
|
||||
handler->close();
|
||||
delete handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
while(res != -1 && conns);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#include <fcntl.h>
|
||||
#include <cstdlib>
|
||||
#include <cassert>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
constexpr std::size_t ev_buffsz = 512;
|
||||
|
||||
#ifdef __linux__
|
||||
#include "epoll.hpp"
|
||||
typedef nntpchan::ev::EpollLoop<ev_buffsz> LoopImpl;
|
||||
#else
|
||||
#ifdef __FreeBSD__
|
||||
#include "kqueue.hpp"
|
||||
typedef nntpchan::ev::KqueueLoop<ev_buffsz> LoopImpl;
|
||||
#else
|
||||
#ifdef __netbsd__
|
||||
typedef nntpchan::ev::KqueueLoop<ev_buffsz> LoopImpl;
|
||||
#else
|
||||
#error "unsupported platform"
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
namespace ev
|
||||
{
|
||||
bool ev::Loop::BindTCP(const sockaddr *addr, ev::io *handler)
|
||||
{
|
||||
assert(handler->acceptable());
|
||||
socklen_t slen;
|
||||
switch (addr->sa_family)
|
||||
{
|
||||
case AF_INET:
|
||||
slen = sizeof(sockaddr_in);
|
||||
break;
|
||||
case AF_INET6:
|
||||
slen = sizeof(sockaddr_in6);
|
||||
break;
|
||||
case AF_UNIX:
|
||||
slen = sizeof(sockaddr_un);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
int fd = socket(addr->sa_family, SOCK_STREAM | SOCK_NONBLOCK, 0);
|
||||
if (fd == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bind(fd, addr, slen) == -1)
|
||||
{
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(fd, 5) == -1)
|
||||
{
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
handler->fd = fd;
|
||||
return TrackConn(handler);
|
||||
}
|
||||
|
||||
bool Loop::SetNonBlocking(ev::io *handler)
|
||||
{
|
||||
return fcntl(handler->fd, F_SETFL, fcntl(handler->fd, F_GETFL, 0) | O_NONBLOCK) != -1;
|
||||
}
|
||||
}
|
||||
|
||||
ev::Loop *NewMainLoop() { return new LoopImpl; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#include <cstring>
|
||||
#include <errno.h>
|
||||
#include <iostream>
|
||||
#include <nntpchan/exec_frontend.hpp>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
ExecFrontend::ExecFrontend(const std::string &fname, char * const* env) : m_Environ(env), m_exec(fname) {}
|
||||
|
||||
ExecFrontend::~ExecFrontend() {}
|
||||
|
||||
void ExecFrontend::ProcessNewMessage(const fs::path &fpath) { Exec({"post", fpath}); }
|
||||
|
||||
bool ExecFrontend::AcceptsNewsgroup(const std::string &newsgroup) { return Exec({"newsgroup", newsgroup}) == 0; }
|
||||
|
||||
bool ExecFrontend::AcceptsMessage(const std::string &msgid) { return Exec({"msgid", msgid}) == 0; }
|
||||
|
||||
int ExecFrontend::Exec(std::deque<std::string> args)
|
||||
{
|
||||
// set up arguments
|
||||
const char **cargs = new char const *[args.size() + 2];
|
||||
std::size_t l = 0;
|
||||
cargs[l++] = m_exec.c_str();
|
||||
while (args.size())
|
||||
{
|
||||
cargs[l++] = args.front().c_str();
|
||||
args.pop_front();
|
||||
}
|
||||
cargs[l] = 0;
|
||||
int retcode = 0;
|
||||
pid_t child = fork();
|
||||
if (child)
|
||||
{
|
||||
waitpid(child, &retcode, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
int r = execve(m_exec.c_str(), (char *const *)cargs, m_Environ);
|
||||
if (r == -1)
|
||||
{
|
||||
std::cout << strerror(errno) << std::endl;
|
||||
exit(errno);
|
||||
}
|
||||
else
|
||||
exit(r);
|
||||
}
|
||||
return retcode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <nntpchan/file_handle.hpp>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
FileHandle_ptr OpenFile(const fs::path &fname, FileMode mode)
|
||||
{
|
||||
std::fstream *f = new std::fstream;
|
||||
if (mode == eRead)
|
||||
{
|
||||
f->open(fname, std::ios::in);
|
||||
}
|
||||
else if (mode == eWrite)
|
||||
{
|
||||
f->open(fname, std::ios::out);
|
||||
}
|
||||
if (f->is_open())
|
||||
return FileHandle_ptr(f);
|
||||
delete f;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
#include <nntpchan/event.hpp>
|
||||
#include <sys/types.h>
|
||||
#include <sys/event.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <errno.h>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
namespace ev
|
||||
{
|
||||
template<size_t bufsz>
|
||||
struct KqueueLoop : public Loop
|
||||
{
|
||||
int kfd;
|
||||
size_t conns;
|
||||
char readbuf[bufsz];
|
||||
|
||||
|
||||
KqueueLoop() : kfd(kqueue()), conns(0)
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
virtual ~KqueueLoop()
|
||||
{
|
||||
::close(kfd);
|
||||
}
|
||||
|
||||
virtual bool TrackConn(ev::io * handler)
|
||||
{
|
||||
struct kevent event;
|
||||
short filter = 0;
|
||||
if(handler->readable() || handler->acceptable())
|
||||
{
|
||||
filter |= EVFILT_READ;
|
||||
}
|
||||
if(handler->writeable())
|
||||
{
|
||||
filter |= EVFILT_WRITE;
|
||||
}
|
||||
EV_SET(&event, handler->fd, filter, EV_ADD | EV_CLEAR, 0, 0, handler);
|
||||
int ret = kevent(kfd, &event, 1, nullptr, 0, nullptr);
|
||||
if(ret == -1) return false;
|
||||
if(event.flags & EV_ERROR)
|
||||
{
|
||||
std::cerr << "KqueueLoop::TrackConn() kevent failed: " << strerror(event.data) << std::endl;
|
||||
return false;
|
||||
}
|
||||
++conns;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void UntrackConn(ev::io * handler)
|
||||
{
|
||||
struct kevent event;
|
||||
short filter = 0;
|
||||
if(handler->readable() || handler->acceptable())
|
||||
{
|
||||
filter |= EVFILT_READ;
|
||||
}
|
||||
if(handler->writeable())
|
||||
{
|
||||
filter |= EVFILT_WRITE;
|
||||
}
|
||||
EV_SET(&event, handler->fd, filter, EV_DELETE, 0, 0, handler);
|
||||
int ret = kevent(kfd, &event, 1, nullptr, 0, nullptr);
|
||||
if(ret == -1 || event.flags & EV_ERROR)
|
||||
std::cerr << "KqueueLoop::UntrackConn() kevent failed: " << strerror(event.data) << std::endl;
|
||||
else
|
||||
--conns;
|
||||
}
|
||||
|
||||
virtual void Run()
|
||||
{
|
||||
struct kevent events[512];
|
||||
struct kevent * event;
|
||||
io * handler;
|
||||
int ret, idx;
|
||||
do
|
||||
{
|
||||
idx = 0;
|
||||
ret = kevent(kfd, nullptr, 0, events, 512, nullptr);
|
||||
if(ret > 0)
|
||||
{
|
||||
while(idx < ret)
|
||||
{
|
||||
event = &events[idx++];
|
||||
handler = static_cast<io *>(event->udata);
|
||||
if(event->flags & EV_EOF)
|
||||
{
|
||||
handler->close();
|
||||
delete handler;
|
||||
continue;
|
||||
}
|
||||
if(event->filter & EVFILT_READ && handler->acceptable())
|
||||
{
|
||||
int backlog = event->data;
|
||||
while(backlog)
|
||||
{
|
||||
handler->accept();
|
||||
--backlog;
|
||||
}
|
||||
}
|
||||
|
||||
if(event->filter & EVFILT_READ && handler->readable())
|
||||
{
|
||||
int readed = 0;
|
||||
size_t readnum = event->data;
|
||||
while(readnum > sizeof(readbuf))
|
||||
{
|
||||
int r = handler->read(readbuf, sizeof(readbuf));
|
||||
if(r > 0)
|
||||
{
|
||||
readnum -= r;
|
||||
readed += r;
|
||||
}
|
||||
else
|
||||
readnum = 0;
|
||||
}
|
||||
if(readnum && readed != -1)
|
||||
{
|
||||
int r = handler->read(readbuf, readnum);
|
||||
if(r > 0)
|
||||
readed += r;
|
||||
else
|
||||
readed = r;
|
||||
}
|
||||
}
|
||||
if(event->filter & EVFILT_WRITE && handler->writeable())
|
||||
{
|
||||
int writespace = 1024;
|
||||
int written = handler->write(writespace);
|
||||
if(written == -1)
|
||||
{
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
{
|
||||
// blocking
|
||||
}
|
||||
else
|
||||
{
|
||||
perror("write()");
|
||||
handler->close();
|
||||
delete handler;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!handler->keepalive())
|
||||
{
|
||||
handler->close();
|
||||
delete handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while(ret != -1);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#include <nntpchan/line.hpp>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
void LineReader::Data(const char *data, ssize_t l)
|
||||
{
|
||||
if (l <= 0)
|
||||
return;
|
||||
m_line << m_leftover;
|
||||
m_leftover = "";
|
||||
m_line << std::string(data, l);
|
||||
|
||||
for (std::string line; std::getline(m_line, line);)
|
||||
{
|
||||
line.erase(std::remove(line.begin(), line.end(), '\r'), line.end());
|
||||
HandleLine(line);
|
||||
}
|
||||
if (m_line)
|
||||
m_leftover = m_line.str();
|
||||
m_line.clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include <nntpchan/mime.hpp>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
bool ReadHeader(const FileHandle_ptr &file, RawHeader &header)
|
||||
{
|
||||
std::string line;
|
||||
while (std::getline(*file, line) && !(line == "\r" || line == ""))
|
||||
{
|
||||
std::string k, v;
|
||||
auto idx = line.find(": ");
|
||||
auto endidx = line.size() - 1;
|
||||
|
||||
while (line[endidx] == '\r')
|
||||
--endidx;
|
||||
|
||||
if (idx != std::string::npos && idx + 2 < endidx)
|
||||
{
|
||||
k = line.substr(0, idx);
|
||||
v = line.substr(idx + 2, endidx);
|
||||
header[k] = v;
|
||||
}
|
||||
}
|
||||
return file->good();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#include <cstring>
|
||||
#include <nntpchan/net.hpp>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
std::string NetAddr::to_string()
|
||||
{
|
||||
std::string str("invalid");
|
||||
const size_t s = 128;
|
||||
char *buff = new char[s];
|
||||
if (inet_ntop(AF_INET6, &addr, buff, sizeof(sockaddr_in6)))
|
||||
{
|
||||
str = std::string(buff);
|
||||
delete[] buff;
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << "[" << str << "]:" << ntohs(addr.sin6_port);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
NetAddr::NetAddr() { std::memset(&addr, 0, sizeof(addr)); }
|
||||
|
||||
NetAddr ParseAddr(const std::string &addr)
|
||||
{
|
||||
NetAddr saddr;
|
||||
auto n = addr.rfind("]:");
|
||||
if (n == std::string::npos)
|
||||
{
|
||||
throw std::runtime_error("invalid address: " + addr);
|
||||
}
|
||||
if (addr[0] != '[')
|
||||
{
|
||||
throw std::runtime_error("invalid address: " + addr);
|
||||
}
|
||||
auto p = addr.substr(n + 2);
|
||||
int port = std::atoi(p.c_str());
|
||||
auto a = addr.substr(0, n);
|
||||
saddr.addr.sin6_port = htons(port);
|
||||
saddr.addr.sin6_family = AF_INET6;
|
||||
inet_pton(AF_INET6, a.c_str(), &saddr.addr);
|
||||
return saddr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <nntpchan/base64.hpp>
|
||||
#include <nntpchan/crypto.hpp>
|
||||
#include <nntpchan/nntp_auth.hpp>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
HashedCredDB::HashedCredDB() : LineReader() {}
|
||||
|
||||
bool HashedCredDB::CheckLogin(const std::string &user, const std::string &passwd)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_access);
|
||||
m_found = false;
|
||||
m_user = user;
|
||||
m_passwd = passwd;
|
||||
m_instream->seekg(0, std::ios::end);
|
||||
const auto l = m_instream->tellg();
|
||||
m_instream->seekg(0, std::ios::beg);
|
||||
char *buff = new char[l];
|
||||
// read file
|
||||
m_instream->read(buff, l);
|
||||
Data(buff, l);
|
||||
delete[] buff;
|
||||
return m_found;
|
||||
}
|
||||
|
||||
bool HashedCredDB::ProcessLine(const std::string &line)
|
||||
{
|
||||
// strip comments
|
||||
auto comment = line.find("#");
|
||||
std::string part = line;
|
||||
for (; comment != std::string::npos; comment = part.find("#"))
|
||||
{
|
||||
if (comment)
|
||||
part = part.substr(0, comment);
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (!part.size())
|
||||
return false; // empty line after comments
|
||||
auto idx = part.find(":");
|
||||
if (idx == std::string::npos)
|
||||
return false; // bad format
|
||||
if (m_user != part.substr(0, idx))
|
||||
return false; // username mismatch
|
||||
part = part.substr(idx + 1);
|
||||
|
||||
idx = part.find(":");
|
||||
if (idx == std::string::npos)
|
||||
return false; // bad format
|
||||
std::string cred = part.substr(0, idx);
|
||||
std::string salt = part.substr(idx + 1);
|
||||
return Hash(m_passwd, salt) == cred;
|
||||
}
|
||||
|
||||
void HashedCredDB::HandleLine(const std::string line)
|
||||
{
|
||||
if (m_found)
|
||||
return;
|
||||
if (ProcessLine(line))
|
||||
m_found = true;
|
||||
}
|
||||
|
||||
void HashedCredDB::SetStream(std::istream *s) { m_instream = s; }
|
||||
|
||||
std::string HashedCredDB::Hash(const std::string &data, const std::string &salt)
|
||||
{
|
||||
SHA512Digest h;
|
||||
std::string d = data + salt;
|
||||
SHA512((const uint8_t *)d.c_str(), d.size(), h);
|
||||
return B64Encode(h.data(), h.size());
|
||||
}
|
||||
|
||||
HashedFileDB::HashedFileDB(const std::string &fname) : m_fname(fname), f(nullptr) {}
|
||||
|
||||
HashedFileDB::~HashedFileDB() {}
|
||||
|
||||
void HashedFileDB::Close()
|
||||
{
|
||||
if (f.is_open())
|
||||
f.close();
|
||||
}
|
||||
|
||||
bool HashedFileDB::Open()
|
||||
{
|
||||
if (!f.is_open())
|
||||
f.open(m_fname);
|
||||
if (f.is_open())
|
||||
{
|
||||
SetStream(&f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <nntpchan/nntp_handler.hpp>
|
||||
#include <nntpchan/sanitize.hpp>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
NNTPServerHandler::NNTPServerHandler(fs::path storage)
|
||||
: LineReader(), m_article(nullptr), m_auth(nullptr), m_store(storage), m_authed(false),
|
||||
m_state(eStateReadCommand)
|
||||
{
|
||||
}
|
||||
|
||||
NNTPServerHandler::~NNTPServerHandler() {}
|
||||
|
||||
void NNTPServerHandler::HandleLine(const std::string line)
|
||||
{
|
||||
if (m_state == eStateReadCommand)
|
||||
{
|
||||
std::deque<std::string> command;
|
||||
std::istringstream s;
|
||||
s.str(line);
|
||||
for (std::string part; std::getline(s, part, ' ');)
|
||||
{
|
||||
if (part.size())
|
||||
command.push_back(part);
|
||||
}
|
||||
if (command.size())
|
||||
HandleCommand(command);
|
||||
else
|
||||
QueueLine("501 Syntax error");
|
||||
}
|
||||
else if (m_state == eStateStoreArticle)
|
||||
{
|
||||
std::string l = line + "\r\n";
|
||||
OnData(l.c_str(), l.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "invalid state" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void NNTPServerHandler::OnData(const char *data, ssize_t l)
|
||||
{
|
||||
if (l <= 0)
|
||||
return;
|
||||
if (m_state == eStateStoreArticle)
|
||||
{
|
||||
std::cerr << "storing " << l << " bytes" << std::endl;
|
||||
if (strncmp(data, ".\r\n", l) == 0)
|
||||
{
|
||||
ArticleObtained();
|
||||
return;
|
||||
}
|
||||
const char *end = strstr(data, "\r\n.\r\n");
|
||||
if (end)
|
||||
{
|
||||
std::size_t diff = end - data;
|
||||
if (m_article)
|
||||
{
|
||||
m_article->write(data, diff + 2);
|
||||
m_article->flush();
|
||||
}
|
||||
ArticleObtained();
|
||||
diff += 5;
|
||||
if (l - diff)
|
||||
Data(end + 5, l - diff);
|
||||
return;
|
||||
}
|
||||
if (m_article)
|
||||
{
|
||||
m_article->write(data, l);
|
||||
m_article->flush();
|
||||
}
|
||||
}
|
||||
else
|
||||
Data(data, l);
|
||||
}
|
||||
|
||||
void NNTPServerHandler::HandleCommand(const std::deque<std::string> &command)
|
||||
{
|
||||
auto cmd = command[0];
|
||||
std::transform(cmd.begin(), cmd.end(), cmd.begin(), ::toupper);
|
||||
std::size_t cmdlen = command.size();
|
||||
for (const auto &part : command)
|
||||
std::cerr << " " << part;
|
||||
std::cerr << std::endl;
|
||||
if (cmd == "QUIT")
|
||||
{
|
||||
Quit();
|
||||
return;
|
||||
}
|
||||
else if (cmd[0] == '5')
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (cmd == "MODE")
|
||||
{
|
||||
if (cmdlen == 2)
|
||||
{
|
||||
// set mode
|
||||
SwitchMode(command[1]);
|
||||
}
|
||||
else if (cmdlen)
|
||||
{
|
||||
// too many arguments
|
||||
QueueLine("500 too many arguments");
|
||||
}
|
||||
else
|
||||
{
|
||||
// get mode
|
||||
QueueLine("500 wrong arguments");
|
||||
}
|
||||
}
|
||||
else if (cmd == "CAPABILITIES")
|
||||
{
|
||||
QueueLine("101 I support the following:");
|
||||
QueueLine("READER");
|
||||
QueueLine("IMPLEMENTATION nntpchan-daemon");
|
||||
QueueLine("VERSION 2");
|
||||
QueueLine("STREAMING");
|
||||
QueueLine(".");
|
||||
}
|
||||
else if (cmd == "CHECK")
|
||||
{
|
||||
if (cmdlen >= 2)
|
||||
{
|
||||
const std::string &msgid = command[1];
|
||||
if (IsValidMessageID(msgid) && m_store.Accept(msgid))
|
||||
{
|
||||
QueueLine("238 " + msgid);
|
||||
}
|
||||
else
|
||||
QueueLine("438 " + msgid);
|
||||
}
|
||||
else
|
||||
QueueLine("501 syntax error");
|
||||
}
|
||||
else if (cmd == "TAKETHIS")
|
||||
{
|
||||
if (cmdlen >= 2)
|
||||
{
|
||||
const std::string &msgid = command[1];
|
||||
if (m_store.Accept(msgid))
|
||||
{
|
||||
m_article = m_store.OpenWrite(msgid);
|
||||
}
|
||||
m_articleName = msgid;
|
||||
EnterState(eStateStoreArticle);
|
||||
return;
|
||||
}
|
||||
QueueLine("501 invalid syntax");
|
||||
}
|
||||
else
|
||||
{
|
||||
// unknown command
|
||||
QueueLine("500 Unknown Command");
|
||||
}
|
||||
}
|
||||
|
||||
void NNTPServerHandler::ArticleObtained()
|
||||
{
|
||||
if (m_article)
|
||||
{
|
||||
m_article->close();
|
||||
m_article = nullptr;
|
||||
m_store.EnsureSymlinks(m_articleName);
|
||||
QueueLine("239 " + m_articleName);
|
||||
std::cerr << "stored " << m_articleName << std::endl;
|
||||
}
|
||||
else
|
||||
QueueLine("439 " + m_articleName);
|
||||
m_articleName = "";
|
||||
EnterState(eStateReadCommand);
|
||||
}
|
||||
|
||||
void NNTPServerHandler::SwitchMode(const std::string &mode)
|
||||
{
|
||||
std::string m = mode;
|
||||
std::transform(m.begin(), m.end(), m.begin(), ::toupper);
|
||||
if (m == "READER")
|
||||
{
|
||||
m_mode = m;
|
||||
if (PostingAllowed())
|
||||
{
|
||||
QueueLine("200 Posting is permitted yo");
|
||||
}
|
||||
else
|
||||
{
|
||||
QueueLine("201 Posting is not permitted yo");
|
||||
}
|
||||
}
|
||||
else if (m == "STREAM")
|
||||
{
|
||||
m_mode = m;
|
||||
if (PostingAllowed())
|
||||
{
|
||||
QueueLine("203 Streaming enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
QueueLine("483 Streaming Denied");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// unknown mode
|
||||
QueueLine("500 Unknown mode");
|
||||
}
|
||||
}
|
||||
|
||||
void NNTPServerHandler::EnterState(State st)
|
||||
{
|
||||
std::cerr << "enter state " << st << std::endl;
|
||||
m_state = st;
|
||||
}
|
||||
|
||||
void NNTPServerHandler::Quit()
|
||||
{
|
||||
EnterState(eStateQuit);
|
||||
QueueLine("205 quitting");
|
||||
}
|
||||
|
||||
bool NNTPServerHandler::ShouldClose() { return m_state == eStateQuit; }
|
||||
|
||||
bool NNTPServerHandler::PostingAllowed() { return m_authed || m_auth == nullptr; }
|
||||
|
||||
void NNTPServerHandler::Greet()
|
||||
{
|
||||
if (PostingAllowed())
|
||||
QueueLine("200 Posting allowed");
|
||||
else
|
||||
QueueLine("201 Posting not allowed");
|
||||
}
|
||||
|
||||
void NNTPServerHandler::SetAuth(CredDB_ptr creds) { m_auth = creds; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <nntpchan/net.hpp>
|
||||
#include <nntpchan/nntp_auth.hpp>
|
||||
#include <nntpchan/nntp_handler.hpp>
|
||||
#include <nntpchan/nntp_server.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
NNTPServer::NNTPServer(ev::Loop *loop) : Server(loop), m_frontend(nullptr) {}
|
||||
|
||||
NNTPServer::~NNTPServer() {}
|
||||
|
||||
IServerConn *NNTPServer::CreateConn(int f)
|
||||
{
|
||||
CredDB_ptr creds;
|
||||
|
||||
std::ifstream i;
|
||||
i.open(m_logindbpath);
|
||||
if (i.is_open())
|
||||
creds = std::make_shared<HashedFileDB>(m_logindbpath);
|
||||
|
||||
NNTPServerHandler *handler = new NNTPServerHandler(m_storagePath);
|
||||
if (creds)
|
||||
handler->SetAuth(creds);
|
||||
|
||||
return new NNTPServerConn(f, this, handler);
|
||||
}
|
||||
|
||||
void NNTPServer::SetLoginDB(const std::string path) { m_logindbpath = path; }
|
||||
|
||||
void NNTPServer::SetStoragePath(const std::string &path) { m_storagePath = path; }
|
||||
|
||||
void NNTPServer::SetInstanceName(const std::string &name) { m_servername = name; }
|
||||
|
||||
void NNTPServer::SetFrontend(Frontend *f) { m_frontend.reset(f); }
|
||||
|
||||
std::string NNTPServer::InstanceName() const { return m_servername; }
|
||||
|
||||
void NNTPServer::OnAcceptError(int status) { std::cerr << "nntpserver::accept() " << strerror(status) << std::endl; }
|
||||
|
||||
void NNTPServerConn::Greet()
|
||||
{
|
||||
IConnHandler *handler = GetHandler();
|
||||
handler->Greet();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <nntpchan/sanitize.hpp>
|
||||
#include <regex>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
std::string NNTPSanitizeLine(const std::string &str)
|
||||
{
|
||||
if (str == ".")
|
||||
return " .";
|
||||
std::string sane;
|
||||
sane += str;
|
||||
const char ch = ' ';
|
||||
std::replace_if(sane.begin(), sane.end(), [](unsigned char ch) -> bool { return iscntrl(ch); }, ch);
|
||||
return sane;
|
||||
}
|
||||
|
||||
std::string ToLower(const std::string &str)
|
||||
{
|
||||
std::string lower = str;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char ch) -> unsigned char { return std::tolower(ch); });
|
||||
return lower;
|
||||
}
|
||||
|
||||
static const std::regex re_ValidMessageID("^<[a-zA-Z0-9$\\._]{2,128}@[a-zA-Z0-9\\-\\.]{2,63}>$");
|
||||
|
||||
bool IsValidMessageID(const std::string &msgid) { return std::regex_search(msgid, re_ValidMessageID) == 1; }
|
||||
|
||||
static const std::regex re_ValidNewsgroup("^[a-zA-Z][a-zA-Z0-9.]{1,128}$");
|
||||
|
||||
bool IsValidNewsgroup(const std::string &msgid) { return std::regex_search(msgid, re_ValidNewsgroup) == 1; }
|
||||
|
||||
std::string StripWhitespaces(const std::string &str)
|
||||
{
|
||||
std::string stripped;
|
||||
for (const auto &ch : str)
|
||||
if (!(std::isspace(ch) || std::iscntrl(ch)))
|
||||
stripped += ch;
|
||||
|
||||
return stripped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <nntpchan/net.hpp>
|
||||
#include <nntpchan/server.hpp>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
Server::Server(ev::Loop *loop) : ev::io(-1), m_Loop(loop) {}
|
||||
|
||||
void Server::close()
|
||||
{
|
||||
auto itr = m_conns.begin();
|
||||
while (itr != m_conns.end())
|
||||
{
|
||||
itr = m_conns.erase(itr);
|
||||
}
|
||||
m_Loop->UntrackConn(this);
|
||||
ev::io::close();
|
||||
}
|
||||
bool Server::Bind(const std::string &addr)
|
||||
{
|
||||
auto saddr = ParseAddr(addr);
|
||||
return m_Loop->BindTCP(saddr, this);
|
||||
}
|
||||
|
||||
void Server::OnAccept(int f)
|
||||
{
|
||||
IServerConn *conn = CreateConn(f);
|
||||
if (!m_Loop->SetNonBlocking(conn))
|
||||
{
|
||||
conn->close();
|
||||
delete conn;
|
||||
}
|
||||
else if (m_Loop->TrackConn(conn))
|
||||
{
|
||||
m_conns.push_back(conn);
|
||||
conn->Greet();
|
||||
conn->write(1024);
|
||||
}
|
||||
else
|
||||
{
|
||||
conn->close();
|
||||
delete conn;
|
||||
}
|
||||
}
|
||||
|
||||
int Server::accept()
|
||||
{
|
||||
int res = ::accept(fd, nullptr, nullptr);
|
||||
if (res == -1)
|
||||
return res;
|
||||
OnAccept(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
void Server::RemoveConn(IServerConn *conn)
|
||||
{
|
||||
auto itr = m_conns.begin();
|
||||
while (itr != m_conns.end())
|
||||
{
|
||||
if (*itr == conn)
|
||||
itr = m_conns.erase(itr);
|
||||
else
|
||||
++itr;
|
||||
}
|
||||
m_Loop->UntrackConn(conn);
|
||||
}
|
||||
|
||||
void IConnHandler::QueueLine(const std::string &line) { m_sendlines.push_back(line + "\r\n"); }
|
||||
|
||||
bool IConnHandler::HasNextLine() { return m_sendlines.size() > 0; }
|
||||
|
||||
std::string IConnHandler::GetNextLine()
|
||||
{
|
||||
std::string line = m_sendlines[0];
|
||||
m_sendlines.pop_front();
|
||||
return line;
|
||||
}
|
||||
|
||||
IServerConn::IServerConn(int fd, Server *parent, IConnHandler *h) : ev::io(fd), m_parent(parent), m_handler(h) {}
|
||||
|
||||
IServerConn::~IServerConn() { delete m_handler; }
|
||||
|
||||
int IServerConn::read(char *buf, size_t sz)
|
||||
{
|
||||
ssize_t readsz = ::read(fd, buf, sz);
|
||||
if (readsz > 0)
|
||||
{
|
||||
m_handler->OnData(buf, readsz);
|
||||
}
|
||||
return readsz;
|
||||
}
|
||||
|
||||
bool IServerConn::keepalive() { return !m_handler->ShouldClose(); }
|
||||
|
||||
int IServerConn::write(size_t avail)
|
||||
{
|
||||
auto leftovers = m_writeLeftover.size();
|
||||
int written = 0;
|
||||
if (leftovers)
|
||||
{
|
||||
if (leftovers > avail)
|
||||
{
|
||||
leftovers = avail;
|
||||
}
|
||||
written = ::write(fd, m_writeLeftover.c_str(), leftovers);
|
||||
if (written > 0)
|
||||
{
|
||||
avail -= written;
|
||||
m_writeLeftover = m_writeLeftover.substr(written);
|
||||
}
|
||||
else
|
||||
{
|
||||
// too much leftovers
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
do
|
||||
{
|
||||
if (!m_handler->HasNextLine())
|
||||
{
|
||||
return written;
|
||||
}
|
||||
auto line = m_handler->GetNextLine();
|
||||
int wrote;
|
||||
if (line.size() <= avail)
|
||||
{
|
||||
wrote = ::write(fd, line.c_str(), line.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto subline = line.substr(0, avail);
|
||||
wrote = ::write(fd, subline.c_str(), subline.size());
|
||||
}
|
||||
if (wrote > 0)
|
||||
{
|
||||
written += wrote;
|
||||
avail -= wrote;
|
||||
m_writeLeftover = line.substr(wrote);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_writeLeftover = line;
|
||||
return -1;
|
||||
}
|
||||
} while (avail > 0);
|
||||
return written;
|
||||
}
|
||||
|
||||
void IServerConn::close()
|
||||
{
|
||||
m_parent->RemoveConn(this);
|
||||
ev::io::close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
SHA-1 in C
|
||||
By Steve Reid <steve@edmweb.com>
|
||||
100% Public Domain
|
||||
|
||||
Test Vectors (from FIPS PUB 180-1)
|
||||
"abc"
|
||||
A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D
|
||||
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
|
||||
84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1
|
||||
A million repetitions of "a"
|
||||
34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F
|
||||
*/
|
||||
|
||||
/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */
|
||||
/* #define SHA1HANDSOFF * Copies data before messing with it. */
|
||||
|
||||
#include "crypto_old.hpp"
|
||||
#include <string>
|
||||
|
||||
extern "C" {
|
||||
#define SHA1HANDSOFF
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* for uint32_t */
|
||||
#include <stdint.h>
|
||||
|
||||
#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
|
||||
|
||||
/* blk0() and blk() perform the initial expand. */
|
||||
/* I got the idea of expanding during the round function from SSLeay */
|
||||
#if BYTE_ORDER == LITTLE_ENDIAN
|
||||
#define blk0(i) (block->l[i] = (rol(block->l[i], 24) & 0xFF00FF00) | (rol(block->l[i], 8) & 0x00FF00FF))
|
||||
#elif BYTE_ORDER == BIG_ENDIAN
|
||||
#define blk0(i) block->l[i]
|
||||
#else
|
||||
#error "Endianness not defined!"
|
||||
#endif
|
||||
#define blk(i) \
|
||||
(block->l[i & 15] = \
|
||||
rol(block->l[(i + 13) & 15] ^ block->l[(i + 8) & 15] ^ block->l[(i + 2) & 15] ^ block->l[i & 15], 1))
|
||||
|
||||
/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */
|
||||
#define R0(v, w, x, y, z, i) \
|
||||
z += ((w & (x ^ y)) ^ y) + blk0(i) + 0x5A827999 + rol(v, 5); \
|
||||
w = rol(w, 30);
|
||||
#define R1(v, w, x, y, z, i) \
|
||||
z += ((w & (x ^ y)) ^ y) + blk(i) + 0x5A827999 + rol(v, 5); \
|
||||
w = rol(w, 30);
|
||||
#define R2(v, w, x, y, z, i) \
|
||||
z += (w ^ x ^ y) + blk(i) + 0x6ED9EBA1 + rol(v, 5); \
|
||||
w = rol(w, 30);
|
||||
#define R3(v, w, x, y, z, i) \
|
||||
z += (((w | x) & y) | (w & x)) + blk(i) + 0x8F1BBCDC + rol(v, 5); \
|
||||
w = rol(w, 30);
|
||||
#define R4(v, w, x, y, z, i) \
|
||||
z += (w ^ x ^ y) + blk(i) + 0xCA62C1D6 + rol(v, 5); \
|
||||
w = rol(w, 30);
|
||||
|
||||
/* Hash a single 512-bit block. This is the core of the algorithm. */
|
||||
|
||||
void SHA1Transform(uint32_t state[5], const unsigned char buffer[64])
|
||||
{
|
||||
uint32_t a, b, c, d, e;
|
||||
|
||||
typedef union {
|
||||
unsigned char c[64];
|
||||
uint32_t l[16];
|
||||
} CHAR64LONG16;
|
||||
|
||||
#ifdef SHA1HANDSOFF
|
||||
CHAR64LONG16 block[1]; /* use array to appear as a pointer */
|
||||
|
||||
memcpy(block, buffer, 64);
|
||||
#else
|
||||
/* The following had better never be used because it causes the
|
||||
* pointer-to-const buffer to be cast into a pointer to non-const.
|
||||
* And the result is written through. I threw a "const" in, hoping
|
||||
* this will cause a diagnostic.
|
||||
*/
|
||||
CHAR64LONG16 *block = (const CHAR64LONG16 *)buffer;
|
||||
#endif
|
||||
/* Copy context->state[] to working vars */
|
||||
a = state[0];
|
||||
b = state[1];
|
||||
c = state[2];
|
||||
d = state[3];
|
||||
e = state[4];
|
||||
/* 4 rounds of 20 operations each. Loop unrolled. */
|
||||
R0(a, b, c, d, e, 0);
|
||||
R0(e, a, b, c, d, 1);
|
||||
R0(d, e, a, b, c, 2);
|
||||
R0(c, d, e, a, b, 3);
|
||||
R0(b, c, d, e, a, 4);
|
||||
R0(a, b, c, d, e, 5);
|
||||
R0(e, a, b, c, d, 6);
|
||||
R0(d, e, a, b, c, 7);
|
||||
R0(c, d, e, a, b, 8);
|
||||
R0(b, c, d, e, a, 9);
|
||||
R0(a, b, c, d, e, 10);
|
||||
R0(e, a, b, c, d, 11);
|
||||
R0(d, e, a, b, c, 12);
|
||||
R0(c, d, e, a, b, 13);
|
||||
R0(b, c, d, e, a, 14);
|
||||
R0(a, b, c, d, e, 15);
|
||||
R1(e, a, b, c, d, 16);
|
||||
R1(d, e, a, b, c, 17);
|
||||
R1(c, d, e, a, b, 18);
|
||||
R1(b, c, d, e, a, 19);
|
||||
R2(a, b, c, d, e, 20);
|
||||
R2(e, a, b, c, d, 21);
|
||||
R2(d, e, a, b, c, 22);
|
||||
R2(c, d, e, a, b, 23);
|
||||
R2(b, c, d, e, a, 24);
|
||||
R2(a, b, c, d, e, 25);
|
||||
R2(e, a, b, c, d, 26);
|
||||
R2(d, e, a, b, c, 27);
|
||||
R2(c, d, e, a, b, 28);
|
||||
R2(b, c, d, e, a, 29);
|
||||
R2(a, b, c, d, e, 30);
|
||||
R2(e, a, b, c, d, 31);
|
||||
R2(d, e, a, b, c, 32);
|
||||
R2(c, d, e, a, b, 33);
|
||||
R2(b, c, d, e, a, 34);
|
||||
R2(a, b, c, d, e, 35);
|
||||
R2(e, a, b, c, d, 36);
|
||||
R2(d, e, a, b, c, 37);
|
||||
R2(c, d, e, a, b, 38);
|
||||
R2(b, c, d, e, a, 39);
|
||||
R3(a, b, c, d, e, 40);
|
||||
R3(e, a, b, c, d, 41);
|
||||
R3(d, e, a, b, c, 42);
|
||||
R3(c, d, e, a, b, 43);
|
||||
R3(b, c, d, e, a, 44);
|
||||
R3(a, b, c, d, e, 45);
|
||||
R3(e, a, b, c, d, 46);
|
||||
R3(d, e, a, b, c, 47);
|
||||
R3(c, d, e, a, b, 48);
|
||||
R3(b, c, d, e, a, 49);
|
||||
R3(a, b, c, d, e, 50);
|
||||
R3(e, a, b, c, d, 51);
|
||||
R3(d, e, a, b, c, 52);
|
||||
R3(c, d, e, a, b, 53);
|
||||
R3(b, c, d, e, a, 54);
|
||||
R3(a, b, c, d, e, 55);
|
||||
R3(e, a, b, c, d, 56);
|
||||
R3(d, e, a, b, c, 57);
|
||||
R3(c, d, e, a, b, 58);
|
||||
R3(b, c, d, e, a, 59);
|
||||
R4(a, b, c, d, e, 60);
|
||||
R4(e, a, b, c, d, 61);
|
||||
R4(d, e, a, b, c, 62);
|
||||
R4(c, d, e, a, b, 63);
|
||||
R4(b, c, d, e, a, 64);
|
||||
R4(a, b, c, d, e, 65);
|
||||
R4(e, a, b, c, d, 66);
|
||||
R4(d, e, a, b, c, 67);
|
||||
R4(c, d, e, a, b, 68);
|
||||
R4(b, c, d, e, a, 69);
|
||||
R4(a, b, c, d, e, 70);
|
||||
R4(e, a, b, c, d, 71);
|
||||
R4(d, e, a, b, c, 72);
|
||||
R4(c, d, e, a, b, 73);
|
||||
R4(b, c, d, e, a, 74);
|
||||
R4(a, b, c, d, e, 75);
|
||||
R4(e, a, b, c, d, 76);
|
||||
R4(d, e, a, b, c, 77);
|
||||
R4(c, d, e, a, b, 78);
|
||||
R4(b, c, d, e, a, 79);
|
||||
/* Add the working vars back into context.state[] */
|
||||
state[0] += a;
|
||||
state[1] += b;
|
||||
state[2] += c;
|
||||
state[3] += d;
|
||||
state[4] += e;
|
||||
/* Wipe variables */
|
||||
a = b = c = d = e = 0;
|
||||
#ifdef SHA1HANDSOFF
|
||||
memset(block, '\0', sizeof(block));
|
||||
#endif
|
||||
}
|
||||
|
||||
/* SHA1Init - Initialize new context */
|
||||
|
||||
void SHA1Init(SHA1_CTX *context)
|
||||
{
|
||||
/* SHA1 initialization constants */
|
||||
context->state[0] = 0x67452301;
|
||||
context->state[1] = 0xEFCDAB89;
|
||||
context->state[2] = 0x98BADCFE;
|
||||
context->state[3] = 0x10325476;
|
||||
context->state[4] = 0xC3D2E1F0;
|
||||
context->count[0] = context->count[1] = 0;
|
||||
}
|
||||
|
||||
/* Run your data through this. */
|
||||
|
||||
void SHA1Update(SHA1_CTX *context, const unsigned char *data, uint32_t len)
|
||||
{
|
||||
uint32_t i;
|
||||
|
||||
uint32_t j;
|
||||
|
||||
j = context->count[0];
|
||||
if ((context->count[0] += len << 3) < j)
|
||||
context->count[1]++;
|
||||
context->count[1] += (len >> 29);
|
||||
j = (j >> 3) & 63;
|
||||
if ((j + len) > 63)
|
||||
{
|
||||
memcpy(&context->buffer[j], data, (i = 64 - j));
|
||||
SHA1Transform(context->state, context->buffer);
|
||||
for (; i + 63 < len; i += 64)
|
||||
{
|
||||
SHA1Transform(context->state, &data[i]);
|
||||
}
|
||||
j = 0;
|
||||
}
|
||||
else
|
||||
i = 0;
|
||||
memcpy(&context->buffer[j], &data[i], len - i);
|
||||
}
|
||||
|
||||
/* Add padding and return the message digest. */
|
||||
|
||||
void SHA1Final(unsigned char digest[20], SHA1_CTX *context)
|
||||
{
|
||||
unsigned i;
|
||||
|
||||
unsigned char finalcount[8];
|
||||
|
||||
unsigned char c;
|
||||
|
||||
#if 0 /* untested "improvement" by DHR */
|
||||
/* Convert context->count to a sequence of bytes
|
||||
* in finalcount. Second element first, but
|
||||
* big-endian order within element.
|
||||
* But we do it all backwards.
|
||||
*/
|
||||
unsigned char *fcp = &finalcount[8];
|
||||
|
||||
for (i = 0; i < 2; i++)
|
||||
{
|
||||
uint32_t t = context->count[i];
|
||||
|
||||
int j;
|
||||
|
||||
for (j = 0; j < 4; t >>= 8, j++)
|
||||
*--fcp = (unsigned char) t}
|
||||
#else
|
||||
for (i = 0; i < 8; i++)
|
||||
{
|
||||
finalcount[i] =
|
||||
(unsigned char)((context->count[(i >= 4 ? 0 : 1)] >> ((3 - (i & 3)) * 8)) & 255); /* Endian independent */
|
||||
}
|
||||
#endif
|
||||
c = 0200;
|
||||
SHA1Update(context, &c, 1);
|
||||
while ((context->count[0] & 504) != 448)
|
||||
{
|
||||
c = 0000;
|
||||
SHA1Update(context, &c, 1);
|
||||
}
|
||||
SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */
|
||||
for (i = 0; i < 20; i++)
|
||||
{
|
||||
digest[i] = (unsigned char)((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255);
|
||||
}
|
||||
/* Wipe variables */
|
||||
memset(context, '\0', sizeof(*context));
|
||||
memset(&finalcount, '\0', sizeof(finalcount));
|
||||
}
|
||||
|
||||
void sha1(uint8_t *hash_out, const uint8_t *str, size_t len)
|
||||
{
|
||||
SHA1_CTX ctx;
|
||||
size_t ii;
|
||||
|
||||
SHA1Init(&ctx);
|
||||
for (ii = 0; ii < len; ii += 1)
|
||||
SHA1Update(&ctx, str + ii, 1);
|
||||
SHA1Final(hash_out, &ctx);
|
||||
}
|
||||
}
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
static inline char nibble_to_char(uint8_t n)
|
||||
{
|
||||
if (n >= 10)
|
||||
return n + 87;
|
||||
else
|
||||
return n + 48;
|
||||
}
|
||||
|
||||
std::string sha1_hex(const std::string &data)
|
||||
{
|
||||
uint8_t digest[20];
|
||||
const uint8_t *ptr = (uint8_t *)data.c_str();
|
||||
sha1(digest, ptr, data.size());
|
||||
std::string out;
|
||||
std::size_t idx = 0;
|
||||
while (idx < 20)
|
||||
{
|
||||
out += nibble_to_char((digest[idx] & 0xf0) >> 8) + nibble_to_char(digest[idx] & 0x0f);
|
||||
++idx;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
#include <any>
|
||||
#include <iostream>
|
||||
#include <nntpchan/file_handle.hpp>
|
||||
#include <nntpchan/mime.hpp>
|
||||
#include <nntpchan/sanitize.hpp>
|
||||
#include <nntpchan/sha1.hpp>
|
||||
#include <nntpchan/staticfile_frontend.hpp>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
StaticFileFrontend::StaticFileFrontend(TemplateEngine *tmpl, const std::string &templateDir, const std::string &outDir,
|
||||
uint32_t pages)
|
||||
: m_TemplateEngine(tmpl), m_TemplateDir(templateDir), m_OutDir(outDir), m_Pages(pages)
|
||||
{
|
||||
}
|
||||
|
||||
StaticFileFrontend::~StaticFileFrontend() {}
|
||||
|
||||
void StaticFileFrontend::ProcessNewMessage(const fs::path &fpath)
|
||||
{
|
||||
std::clog << "process message " << fpath << std::endl;
|
||||
auto file = OpenFile(fpath, eRead);
|
||||
if (file)
|
||||
{
|
||||
// read header
|
||||
RawHeader header;
|
||||
if (!ReadHeader(file, header))
|
||||
{
|
||||
std::clog << "failed to read mime header" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// read body
|
||||
|
||||
auto findMsgidFunc = [](const std::pair<std::string, std::string> &item) -> bool {
|
||||
auto lower = ToLower(item.first);
|
||||
return (lower == "message-id") || (lower == "messageid");
|
||||
};
|
||||
|
||||
auto msgid_itr = std::find_if(header.begin(), header.end(), findMsgidFunc);
|
||||
if (msgid_itr == std::end(header))
|
||||
{
|
||||
std::clog << "no message id for file " << fpath << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string msgid = StripWhitespaces(msgid_itr->second);
|
||||
|
||||
if (!IsValidMessageID(msgid))
|
||||
{
|
||||
std::clog << "invalid message-id: " << msgid << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string rootmsgid;
|
||||
|
||||
auto findReferences = [](const std::pair<std::string, std::string> &item) -> bool {
|
||||
auto lower = ToLower(item.first);
|
||||
return lower == "references";
|
||||
};
|
||||
|
||||
auto references_itr = std::find_if(header.begin(), header.end(), findReferences);
|
||||
if (references_itr == std::end(header) || StripWhitespaces(references_itr->second).size() == 0)
|
||||
{
|
||||
rootmsgid = msgid;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto &s = references_itr->second;
|
||||
auto checkfunc = [](unsigned char ch) -> bool { return std::isspace(ch) || std::iscntrl(ch); };
|
||||
if (std::count_if(s.begin(), s.end(), checkfunc))
|
||||
{
|
||||
/** split off first element */
|
||||
auto idx = std::find_if(s.begin(), s.end(), checkfunc);
|
||||
rootmsgid = s.substr(0, s.find(*idx));
|
||||
}
|
||||
else
|
||||
{
|
||||
rootmsgid = references_itr->second;
|
||||
}
|
||||
}
|
||||
|
||||
std::string rootmsgid_hash = sha1_hex(rootmsgid);
|
||||
|
||||
std::set<std::string> newsgroups_list;
|
||||
|
||||
auto findNewsgroupsFunc = [](const std::pair<std::string, std::string> &item) -> bool {
|
||||
return ToLower(item.first) == "newsgroups";
|
||||
};
|
||||
|
||||
auto group = std::find_if(header.begin(), header.end(), findNewsgroupsFunc);
|
||||
if (group == std::end(header))
|
||||
{
|
||||
std::clog << "no newsgroups header" << std::endl;
|
||||
return;
|
||||
}
|
||||
std::istringstream input(group->second);
|
||||
|
||||
std::string newsgroup;
|
||||
while (std::getline(input, newsgroup, ' '))
|
||||
{
|
||||
if (IsValidNewsgroup(newsgroup))
|
||||
newsgroups_list.insert(newsgroup);
|
||||
}
|
||||
|
||||
fs::path threadFilePath = m_OutDir / fs::path("thread-" + rootmsgid_hash + ".html");
|
||||
nntpchan::model::Thread thread;
|
||||
|
||||
if (!m_MessageDB)
|
||||
{
|
||||
std::clog << "no message database" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_MessageDB->LoadThread(thread, rootmsgid))
|
||||
{
|
||||
std::clog << "cannot find thread with root " << rootmsgid << std::endl;
|
||||
return;
|
||||
}
|
||||
if (m_TemplateEngine)
|
||||
{
|
||||
FileHandle_ptr out = OpenFile(threadFilePath, eWrite);
|
||||
if (!out || !m_TemplateEngine->WriteThreadPage(thread, out))
|
||||
{
|
||||
std::clog << "failed to write " << threadFilePath << std::endl;
|
||||
return;
|
||||
}
|
||||
}
|
||||
nntpchan::model::BoardPage page;
|
||||
for (const auto &name : newsgroups_list)
|
||||
{
|
||||
uint32_t pageno = 0;
|
||||
while (pageno < m_Pages)
|
||||
{
|
||||
page.threads.clear();
|
||||
if (!m_MessageDB->LoadBoardPage(page, name, 10, m_Pages))
|
||||
{
|
||||
std::clog << "cannot load board page " << pageno << " for " << name << std::endl;
|
||||
break;
|
||||
}
|
||||
fs::path boardPageFilename(name + "-" + std::to_string(pageno) + ".html");
|
||||
if (m_TemplateEngine)
|
||||
{
|
||||
fs::path outfile = m_OutDir / boardPageFilename;
|
||||
FileHandle_ptr out = OpenFile(outfile, eWrite);
|
||||
if (out)
|
||||
m_TemplateEngine->WriteBoardPage(page, out);
|
||||
else
|
||||
std::clog << "failed to open board page " << outfile << std::endl;
|
||||
}
|
||||
|
||||
++pageno;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool StaticFileFrontend::AcceptsNewsgroup(const std::string &newsgroup) { return IsValidNewsgroup(newsgroup); }
|
||||
|
||||
bool StaticFileFrontend::AcceptsMessage(const std::string &msgid) { return IsValidMessageID(msgid); }
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#include <cassert>
|
||||
#include <nntpchan/crypto.hpp>
|
||||
#include <nntpchan/sanitize.hpp>
|
||||
#include <nntpchan/storage.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
const fs::path posts_skiplist_dir = "posts";
|
||||
const fs::path threads_skiplist_dir = "threads";
|
||||
|
||||
ArticleStorage::ArticleStorage(const fs::path &fpath) { SetPath(fpath); }
|
||||
|
||||
ArticleStorage::~ArticleStorage() {}
|
||||
|
||||
void ArticleStorage::SetPath(const fs::path &fpath)
|
||||
{
|
||||
basedir = fpath;
|
||||
fs::create_directories(basedir);
|
||||
assert(init_skiplist(posts_skiplist_dir));
|
||||
assert(init_skiplist(threads_skiplist_dir));
|
||||
errno = 0;
|
||||
}
|
||||
|
||||
bool ArticleStorage::init_skiplist(const std::string &subdir) const
|
||||
{
|
||||
fs::path skiplist = skiplist_root(subdir);
|
||||
const auto subdirs = {
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
|
||||
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '2', '3', '4', '5', '6', '7',
|
||||
};
|
||||
for (const auto &s : subdirs)
|
||||
fs::create_directories(skiplist / std::string(&s, 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ArticleStorage::Accept(const std::string &msgid) const
|
||||
{
|
||||
if (!IsValidMessageID(msgid))
|
||||
return false;
|
||||
auto p = MessagePath(msgid);
|
||||
bool ret = !fs::exists(p);
|
||||
errno = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
fs::path ArticleStorage::MessagePath(const std::string &msgid) const { return basedir / msgid; }
|
||||
|
||||
FileHandle_ptr ArticleStorage::OpenRead(const std::string &msgid) const { return OpenFile(MessagePath(msgid), eRead); }
|
||||
|
||||
FileHandle_ptr ArticleStorage::OpenWrite(const std::string &msgid) const
|
||||
{
|
||||
return OpenFile(MessagePath(msgid), eWrite);
|
||||
}
|
||||
|
||||
bool ArticleStorage::LoadBoardPage(BoardPage &board, const std::string &newsgroup, uint32_t perpage,
|
||||
uint32_t page) const
|
||||
{
|
||||
(void)board;
|
||||
(void)newsgroup;
|
||||
(void)perpage;
|
||||
(void)page;
|
||||
return false;
|
||||
}
|
||||
bool ArticleStorage::FindThreadByHash(const std::string &hashhex, std::string &msgid) const
|
||||
{
|
||||
(void)hashhex;
|
||||
(void)msgid;
|
||||
return false;
|
||||
}
|
||||
bool ArticleStorage::LoadThread(Thread &thread, const std::string &rootmsgid) const
|
||||
{
|
||||
(void)thread;
|
||||
(void)rootmsgid;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** ensure symlinks are formed for this article by message id */
|
||||
void ArticleStorage::EnsureSymlinks(const std::string &msgid) const
|
||||
{
|
||||
std::string msgidhash = Blake2B_base32(msgid);
|
||||
auto skip = skiplist_dir(skiplist_root(posts_skiplist_dir), msgidhash) / msgidhash;
|
||||
auto path = fs::path("..") / fs::path("..") / fs::path("..") / MessagePath(msgid);
|
||||
fs::create_symlink(path, skip);
|
||||
errno = 0;
|
||||
}
|
||||
|
||||
fs::path ArticleStorage::skiplist_root(const std::string &name) const { return basedir / name; }
|
||||
fs::path ArticleStorage::skiplist_dir(const fs::path &root, const std::string &name) const
|
||||
{
|
||||
return root / name.substr(0, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <iostream>
|
||||
#include <nntpchan/sanitize.hpp>
|
||||
#include <nntpchan/template_engine.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
struct StdTemplateEngine : public TemplateEngine
|
||||
{
|
||||
struct RenderContext
|
||||
{
|
||||
|
||||
bool Load(const fs::path & path)
|
||||
{
|
||||
// clear out previous data
|
||||
m_Data.clear();
|
||||
// open file
|
||||
std::ifstream f;
|
||||
f.open(path);
|
||||
if(f.is_open())
|
||||
{
|
||||
for(std::string line; std::getline(f, line, '\n');)
|
||||
{
|
||||
m_Data += line + "\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool Render(const FileHandle_ptr & out) const = 0;
|
||||
|
||||
std::string m_Data;
|
||||
};
|
||||
|
||||
struct BoardRenderContext : public RenderContext
|
||||
{
|
||||
const nntpchan::model::BoardPage & m_Page;
|
||||
|
||||
BoardRenderContext(const nntpchan::model::BoardPage & page) : m_Page(page) {};
|
||||
|
||||
virtual bool Render(const FileHandle_ptr & out) const
|
||||
{
|
||||
*out << m_Data;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct ThreadRenderContext : public RenderContext
|
||||
{
|
||||
const nntpchan::model::Thread & m_Thread;
|
||||
|
||||
ThreadRenderContext(const nntpchan::model::Thread & thread) : m_Thread(thread) {};
|
||||
|
||||
virtual bool Render(const FileHandle_ptr & out) const
|
||||
{
|
||||
*out << m_Data;
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
bool WriteBoardPage(const nntpchan::model::BoardPage & page, const FileHandle_ptr & out)
|
||||
{
|
||||
BoardRenderContext ctx(page);
|
||||
if(ctx.Load("board.html"))
|
||||
{
|
||||
return ctx.Render(out);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WriteThreadPage(const nntpchan::model::Thread & thread, const FileHandle_ptr & out)
|
||||
{
|
||||
ThreadRenderContext ctx(thread);
|
||||
if(ctx.Load("thread.html"))
|
||||
{
|
||||
return ctx.Render(out);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
TemplateEngine *CreateTemplateEngine(const std::string &dialect)
|
||||
{
|
||||
auto d = ToLower(dialect);
|
||||
if (d == "std")
|
||||
return new StdTemplateEngine;
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[nntp]
|
||||
instance_name=nntp.server.tld
|
||||
bind=[::]:1199
|
||||
authdb=auth.txt
|
||||
|
||||
[articles]
|
||||
store_path=./storage/
|
||||
@@ -0,0 +1,14 @@
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <nntpchan/exec_frontend.hpp>
|
||||
#include <nntpchan/sanitize.hpp>
|
||||
|
||||
int main(int, char *[], char * argenv[])
|
||||
{
|
||||
nntpchan::Frontend_ptr f(new nntpchan::ExecFrontend("./contrib/nntpchan.sh", argenv));
|
||||
assert(f->AcceptsMessage("<test@server>"));
|
||||
assert(f->AcceptsNewsgroup("overchan.test"));
|
||||
assert(nntpchan::IsValidMessageID("<test@test>"));
|
||||
assert(!nntpchan::IsValidMessageID("asd"));
|
||||
std::cout << "all good" << std::endl;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <nntpchan/base64.hpp>
|
||||
#include <nntpchan/crypto.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sodium.h>
|
||||
#include <string>
|
||||
|
||||
static void print_help(const std::string &exename)
|
||||
{
|
||||
std::cout << "usage: " << exename << " [help|gen|check]" << std::endl;
|
||||
}
|
||||
|
||||
static void gen_passwd(const std::string &username, const std::string &passwd)
|
||||
{
|
||||
std::array<uint8_t, 8> random;
|
||||
randombytes_buf(random.data(), random.size());
|
||||
std::string salt = nntpchan::B64Encode(random.data(), random.size());
|
||||
std::string cred = passwd + salt;
|
||||
nntpchan::SHA512Digest d;
|
||||
nntpchan::SHA512((const uint8_t *)cred.c_str(), cred.size(), d);
|
||||
std::string hash = nntpchan::B64Encode(d.data(), d.size());
|
||||
std::cout << username << ":" << hash << ":" << salt << std::endl;
|
||||
}
|
||||
|
||||
static bool check_cred(const std::string &cred, const std::string &passwd)
|
||||
{
|
||||
auto idx = cred.find(":");
|
||||
if (idx == std::string::npos || idx == 0)
|
||||
return false;
|
||||
std::string part = cred.substr(idx + 1);
|
||||
idx = part.find(":");
|
||||
if (idx == std::string::npos || idx == 0)
|
||||
return false;
|
||||
std::string salt = part.substr(idx + 1);
|
||||
std::string hash = part.substr(0, idx);
|
||||
std::vector<uint8_t> h;
|
||||
if (!nntpchan::B64Decode(hash, h))
|
||||
return false;
|
||||
nntpchan::SHA512Digest d;
|
||||
std::string l = passwd + salt;
|
||||
nntpchan::SHA512((const uint8_t *)l.data(), l.size(), d);
|
||||
return std::memcmp(h.data(), d.data(), d.size()) == 0;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
assert(sodium_init() == 0);
|
||||
if (argc == 1)
|
||||
{
|
||||
print_help(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
std::string cmd(argv[1]);
|
||||
if (cmd == "help")
|
||||
{
|
||||
print_help(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
if (cmd == "gen")
|
||||
{
|
||||
if (argc == 4)
|
||||
{
|
||||
gen_passwd(argv[2], argv[3]);
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "usage: " << argv[0] << " gen username password" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (cmd == "check")
|
||||
{
|
||||
std::string cred;
|
||||
std::cout << "credential: ";
|
||||
if (!std::getline(std::cin, cred))
|
||||
{
|
||||
std::cout << "read error" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::string passwd;
|
||||
std::cout << "password: ";
|
||||
if (!std::getline(std::cin, passwd))
|
||||
{
|
||||
std::cout << "read error" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (check_cred(cred, passwd))
|
||||
{
|
||||
std::cout << "okay" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
std::cout << "bad login" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
print_help(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <nntpchan/exec_frontend.hpp>
|
||||
#include <nntpchan/sanitize.hpp>
|
||||
|
||||
int main(int, char *[], char * argenv[])
|
||||
{
|
||||
nntpchan::Frontend_ptr f(new nntpchan::ExecFrontend("./contrib/nntpchan.sh", argenv));
|
||||
assert(nntpchan::IsValidMessageID("<a28a71493831188@web.oniichan.onion>"));
|
||||
assert(f->AcceptsNewsgroup("overchan.test"));
|
||||
std::cout << "all good" << std::endl;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
;; thanks stack overflow
|
||||
;; https://stackoverflow.com/questions/4012321/how-can-i-access-the-path-to-the-current-directory-in-an-emacs-directory-variabl
|
||||
((nil . ((eval . (set (make-local-variable 'my-project-path)
|
||||
(file-name-directory
|
||||
(let ((d (dir-locals-find-file ".")))
|
||||
(if (stringp d) d (car d))))))
|
||||
(eval . (setenv "GOPATH" my-project-path))
|
||||
(eval . (message "Project directory set to `%s'." my-project-path)))))
|
||||
@@ -0,0 +1 @@
|
||||
nntpchand
|
||||
@@ -0,0 +1,14 @@
|
||||
REPO=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
all: clean build
|
||||
|
||||
build: nntpchand
|
||||
|
||||
nntpchand:
|
||||
GOROOT=$(GOROOT) GOPATH=$(REPO) go build -v
|
||||
|
||||
test:
|
||||
GOROOT=$(GOROOT) GOPATH=$(REPO) go test ./...
|
||||
|
||||
clean:
|
||||
GOPATH=$(REPO) go clean -v
|
||||
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"nntpchan/cmd/nntpchan"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nntpchan.Main()
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package nntpchan
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"net"
|
||||
_ "net/http/pprof"
|
||||
"nntpchan/lib/config"
|
||||
"nntpchan/lib/database"
|
||||
"nntpchan/lib/frontend"
|
||||
"nntpchan/lib/nntp"
|
||||
"nntpchan/lib/store"
|
||||
"nntpchan/lib/webhooks"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type runStatus struct {
|
||||
nntpListener net.Listener
|
||||
run bool
|
||||
done chan error
|
||||
}
|
||||
|
||||
func (st *runStatus) Stop() {
|
||||
st.run = false
|
||||
if st.nntpListener != nil {
|
||||
st.nntpListener.Close()
|
||||
}
|
||||
st.nntpListener = nil
|
||||
log.Info("stopping daemon process")
|
||||
}
|
||||
|
||||
func Main() {
|
||||
st := &runStatus{
|
||||
run: true,
|
||||
done: make(chan error),
|
||||
}
|
||||
log.Info("starting up nntpchan...")
|
||||
cfgFname := "nntpchan.json"
|
||||
conf, err := config.Ensure(cfgFname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if conf.Log == "debug" {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
sconfig := conf.Store
|
||||
|
||||
if sconfig == nil {
|
||||
log.Fatal("no article storage configured")
|
||||
}
|
||||
|
||||
nconfig := conf.NNTP
|
||||
|
||||
if nconfig == nil {
|
||||
log.Fatal("no nntp server configured")
|
||||
}
|
||||
|
||||
dconfig := conf.Database
|
||||
|
||||
if dconfig == nil {
|
||||
log.Fatal("no database configured")
|
||||
}
|
||||
|
||||
// create nntp server
|
||||
nserv := nntp.NewServer()
|
||||
nserv.Config = nconfig
|
||||
nserv.Feeds = conf.Feeds
|
||||
|
||||
if nconfig.LoginsFile != "" {
|
||||
nserv.Auth = nntp.FlatfileAuth(nconfig.LoginsFile)
|
||||
}
|
||||
|
||||
// create article storage
|
||||
nserv.Storage, err = store.NewFilesytemStorage(sconfig.Path, true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if conf.WebHooks != nil && len(conf.WebHooks) > 0 {
|
||||
// put webhooks into nntp server event hooks
|
||||
nserv.Hooks = webhooks.NewWebhooks(conf.WebHooks, nserv.Storage)
|
||||
}
|
||||
|
||||
if conf.NNTPHooks != nil && len(conf.NNTPHooks) > 0 {
|
||||
var hooks nntp.MulitHook
|
||||
if nserv.Hooks != nil {
|
||||
hooks = append(hooks, nserv.Hooks)
|
||||
}
|
||||
for _, h := range conf.NNTPHooks {
|
||||
hooks = append(hooks, nntp.NewHook(h))
|
||||
}
|
||||
nserv.Hooks = hooks
|
||||
}
|
||||
var frontends []frontend.Frontend
|
||||
var db database.Database
|
||||
for _, fconf := range conf.Frontends {
|
||||
var f frontend.Frontend
|
||||
f, err = frontend.NewHTTPFrontend(&fconf, db, nserv.Storage)
|
||||
if err == nil {
|
||||
log.Infof("serving frontend %s", f.Name())
|
||||
go f.Serve()
|
||||
frontends = append(frontends, f)
|
||||
} else {
|
||||
log.Fatalf("failed to set up frontend %s: %s", fconf.Name(), err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// start persisting feeds
|
||||
go nserv.PersistFeeds()
|
||||
|
||||
// handle signals
|
||||
sigchnl := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchnl, syscall.SIGHUP, os.Interrupt)
|
||||
go func() {
|
||||
for {
|
||||
s := <-sigchnl
|
||||
if s == syscall.SIGHUP {
|
||||
// handle SIGHUP
|
||||
conf, err := config.Ensure(cfgFname)
|
||||
if err == nil {
|
||||
log.Infof("reloading config: %s", cfgFname)
|
||||
nserv.ReloadServer(conf.NNTP)
|
||||
nserv.ReloadFeeds(conf.Feeds)
|
||||
nserv.ReloadStorage(conf.Store)
|
||||
for idx := range frontends {
|
||||
f := frontends[idx]
|
||||
for i := range conf.Frontends {
|
||||
c := conf.Frontends[i]
|
||||
if c.Name() == f.Name() {
|
||||
// TODO: inject storage config?
|
||||
f.Reload(&c)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("failed to reload config: %s", err)
|
||||
}
|
||||
} else if s == os.Interrupt {
|
||||
// handle interrupted, clean close
|
||||
st.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
var err error
|
||||
for st.run {
|
||||
var nl net.Listener
|
||||
naddr := conf.NNTP.Bind
|
||||
log.Infof("Bind nntp server to %s", naddr)
|
||||
nl, err = net.Listen("tcp", naddr)
|
||||
if err == nil {
|
||||
st.nntpListener = nl
|
||||
err = nserv.Serve(nl)
|
||||
if err != nil {
|
||||
nl.Close()
|
||||
log.Errorf("nntpserver.serve() %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Errorf("nntp server net.Listen failed: %s", err.Error())
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
st.done <- err
|
||||
}()
|
||||
e := <-st.done
|
||||
if e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
log.Info("ended")
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
// simple nntp server
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"net"
|
||||
"nntpchan/lib/config"
|
||||
"nntpchan/lib/nntp"
|
||||
"nntpchan/lib/store"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
log.Info("starting NNTP server...")
|
||||
conf, err := config.Ensure("settings.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if conf.Log == "debug" {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
serv := &nntp.Server{
|
||||
Config: conf.NNTP,
|
||||
Feeds: conf.Feeds,
|
||||
}
|
||||
serv.Storage, err = store.NewFilesytemStorage(conf.Store.Path, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
l, err := net.Listen("tcp", conf.NNTP.Bind)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Info("listening on ", l.Addr())
|
||||
err = serv.Serve(l)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//
|
||||
// server admin panel
|
||||
//
|
||||
package admin
|
||||
@@ -0,0 +1,16 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"nntpchan/lib/model"
|
||||
)
|
||||
|
||||
// json api
|
||||
type API interface {
|
||||
MakePost(p model.Post)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// json api
|
||||
package api
|
||||
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// api server
|
||||
type Server struct {
|
||||
}
|
||||
|
||||
func (s *Server) HandlePing(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
// inject api routes
|
||||
func (s *Server) SetupRoutes(r *mux.Router) {
|
||||
// setup api pinger
|
||||
r.Path("/ping").HandlerFunc(s.HandlePing)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package config
|
||||
|
||||
import "regexp"
|
||||
|
||||
// configration for local article policies
|
||||
type ArticleConfig struct {
|
||||
// explicitly allow these newsgroups (regexp)
|
||||
AllowGroups []string `json:"whitelist"`
|
||||
// explicitly disallow these newsgroups (regexp)
|
||||
DisallowGroups []string `json:"blacklist"`
|
||||
// only allow explicitly allowed groups
|
||||
ForceWhitelist bool `json:"force-whitelist"`
|
||||
// allow anonymous posts?
|
||||
AllowAnon bool `json:"anon"`
|
||||
// allow attachments?
|
||||
AllowAttachments bool `json:"attachments"`
|
||||
// allow anonymous attachments?
|
||||
AllowAnonAttachments bool `json:"anon-attachments"`
|
||||
}
|
||||
|
||||
func (c *ArticleConfig) AllowGroup(group string) bool {
|
||||
|
||||
for _, g := range c.DisallowGroups {
|
||||
r := regexp.MustCompile(g)
|
||||
if r.MatchString(group) && c.ForceWhitelist {
|
||||
// disallowed
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// check allowed groups first
|
||||
for _, g := range c.AllowGroups {
|
||||
r := regexp.MustCompile(g)
|
||||
if r.MatchString(g) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return !c.ForceWhitelist
|
||||
}
|
||||
|
||||
// allow an article?
|
||||
func (c *ArticleConfig) Allow(msgid, group string, anon, attachment bool) bool {
|
||||
|
||||
// check attachment policy
|
||||
if c.AllowGroup(group) {
|
||||
allow := true
|
||||
// no anon ?
|
||||
if anon && !c.AllowAnon {
|
||||
allow = false
|
||||
}
|
||||
// no attachments ?
|
||||
if allow && attachment && !c.AllowAttachments {
|
||||
allow = false
|
||||
}
|
||||
// no anon attachments ?
|
||||
if allow && attachment && anon && !c.AllowAnonAttachments {
|
||||
allow = false
|
||||
}
|
||||
return allow
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var DefaultArticlePolicy = ArticleConfig{
|
||||
AllowGroups: []string{"ctl", "overchan.test"},
|
||||
DisallowGroups: []string{"overchan.cp"},
|
||||
ForceWhitelist: false,
|
||||
AllowAnon: true,
|
||||
AllowAttachments: true,
|
||||
AllowAnonAttachments: false,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package config
|
||||
|
||||
// caching interface configuration
|
||||
type CacheConfig struct {
|
||||
// backend cache driver name
|
||||
Backend string `json:"backend"`
|
||||
// address for cache
|
||||
Addr string `json:"addr"`
|
||||
// username for login
|
||||
User string `json:"user"`
|
||||
// password for login
|
||||
Password string `json:"password"`
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// main configuration
|
||||
type Config struct {
|
||||
// nntp server configuration
|
||||
NNTP *NNTPServerConfig `json:"nntp"`
|
||||
// log level
|
||||
Log string `json:"log"`
|
||||
// article storage config
|
||||
Store *StoreConfig `json:"storage"`
|
||||
// web hooks to call
|
||||
WebHooks []*WebhookConfig `json:"webhooks"`
|
||||
// external scripts to call
|
||||
NNTPHooks []*NNTPHookConfig `json:"nntphooks"`
|
||||
// database backend configuration
|
||||
Database *DatabaseConfig `json:"db"`
|
||||
// list of feeds to add on runtime
|
||||
Feeds []*FeedConfig `json:"feeds"`
|
||||
// frontend config
|
||||
Frontends []FrontendConfig `json:"frontends"`
|
||||
// unexported fields ...
|
||||
|
||||
// absolute filepath to configuration
|
||||
fpath string
|
||||
}
|
||||
|
||||
// default configuration
|
||||
var DefaultConfig = Config{
|
||||
Store: &DefaultStoreConfig,
|
||||
NNTP: &DefaultNNTPConfig,
|
||||
Database: &DefaultDatabaseConfig,
|
||||
WebHooks: []*WebhookConfig{DefaultWebHookConfig},
|
||||
NNTPHooks: []*NNTPHookConfig{DefaultNNTPHookConfig},
|
||||
Feeds: DefaultFeeds,
|
||||
Frontends: []FrontendConfig{DefaultFrontendConfig},
|
||||
Log: "debug",
|
||||
}
|
||||
|
||||
// reload configuration
|
||||
func (c *Config) Reload() (err error) {
|
||||
var b []byte
|
||||
b, err = ioutil.ReadFile(c.fpath)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(b, c)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ensure that a config file exists
|
||||
// creates one if it does not exist
|
||||
func Ensure(fname string) (cfg *Config, err error) {
|
||||
_, err = os.Stat(fname)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
var d []byte
|
||||
d, err = json.Marshal(&DefaultConfig)
|
||||
if err == nil {
|
||||
b := new(bytes.Buffer)
|
||||
err = json.Indent(b, d, "", " ")
|
||||
if err == nil {
|
||||
err = ioutil.WriteFile(fname, b.Bytes(), 0600)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
cfg, err = Load(fname)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// load configuration file
|
||||
func Load(fname string) (cfg *Config, err error) {
|
||||
cfg = new(Config)
|
||||
cfg.fpath = fname
|
||||
err = cfg.Reload()
|
||||
if err != nil {
|
||||
cfg = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package config
|
||||
|
||||
type DatabaseConfig struct {
|
||||
// url or address for database connector
|
||||
Addr string `json:"addr"`
|
||||
// password to use
|
||||
Password string `json:"password"`
|
||||
// username to use
|
||||
Username string `json:"username"`
|
||||
// type of database to use
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
var DefaultDatabaseConfig = DatabaseConfig{
|
||||
Type: "postgres",
|
||||
Addr: "/var/run/postgresql",
|
||||
Password: "",
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//
|
||||
// package for parsing config files
|
||||
//
|
||||
package config
|
||||
@@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
// configuration for 1 nntp feed
|
||||
type FeedConfig struct {
|
||||
// feed's policy, filters articles
|
||||
Policy *ArticleConfig `json:"policy"`
|
||||
// remote server's address
|
||||
Addr string `json:"addr"`
|
||||
// proxy server config
|
||||
Proxy *ProxyConfig `json:"proxy"`
|
||||
// nntp username to log in with
|
||||
Username string `json:"username"`
|
||||
// nntp password to use when logging in
|
||||
Password string `json:"password"`
|
||||
// do we want to use tls?
|
||||
TLS bool `json:"tls"`
|
||||
// the name of this feed
|
||||
Name string `json:"name"`
|
||||
// how often to pull articles from the server in minutes
|
||||
// 0 for never
|
||||
PullInterval int `json:"pull"`
|
||||
}
|
||||
|
||||
var DuummyFeed = FeedConfig{
|
||||
Policy: &DefaultArticlePolicy,
|
||||
Addr: "nntp.dummy.tld:1119",
|
||||
Proxy: &DefaultTorProxy,
|
||||
Name: "dummy",
|
||||
}
|
||||
|
||||
var DefaultFeeds = []*FeedConfig{
|
||||
&DuummyFeed,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type FrontendConfig struct {
|
||||
// bind to address
|
||||
BindAddr string `json:"bind"`
|
||||
// frontend cache
|
||||
Cache *CacheConfig `json:"cache"`
|
||||
// frontend ssl settings
|
||||
SSL *SSLSettings `json:"ssl"`
|
||||
// static files directory
|
||||
Static string `json:"static_dir"`
|
||||
// http middleware configuration
|
||||
Middleware *MiddlewareConfig `json:"middleware"`
|
||||
// storage config
|
||||
Storage *StoreConfig `json:"-"`
|
||||
}
|
||||
|
||||
func (cfg *FrontendConfig) Name() string {
|
||||
return fmt.Sprintf("frontend-%s", cfg.BindAddr)
|
||||
}
|
||||
|
||||
// default Frontend Configuration
|
||||
var DefaultFrontendConfig = FrontendConfig{
|
||||
BindAddr: "127.0.0.1:18888",
|
||||
Static: "./files/static/",
|
||||
Middleware: &DefaultMiddlewareConfig,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package config
|
||||
|
||||
// config for external callback for nntp articles
|
||||
type NNTPHookConfig struct {
|
||||
// name of hook
|
||||
Name string `json:"name"`
|
||||
// executable script path to be called with arguments: /path/to/article
|
||||
Exec string `json:"exec"`
|
||||
}
|
||||
|
||||
// default dummy hook
|
||||
var DefaultNNTPHookConfig = &NNTPHookConfig{
|
||||
Name: "dummy",
|
||||
Exec: "/bin/true",
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
// configuration for http middleware
|
||||
type MiddlewareConfig struct {
|
||||
// middleware type, currently just 1 is available: overchan
|
||||
Type string `json:"type"`
|
||||
// directory for our html templates
|
||||
Templates string `json:"templates_dir"`
|
||||
// directory for static files
|
||||
StaticDir string `json:"static_dir"`
|
||||
}
|
||||
|
||||
var DefaultMiddlewareConfig = MiddlewareConfig{
|
||||
Type: "overchan",
|
||||
Templates: "./files/templates/overchan/",
|
||||
StaticDir: "./files/",
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
type NNTPServerConfig struct {
|
||||
// address to bind to
|
||||
Bind string `json:"bind"`
|
||||
// name of the nntp server
|
||||
Name string `json:"name"`
|
||||
// default inbound article policy
|
||||
Article *ArticleConfig `json:"policy"`
|
||||
// do we allow anonymous NNTP sync?
|
||||
AnonNNTP bool `json:"anon-nntp"`
|
||||
// ssl settings for nntp
|
||||
SSL *SSLSettings
|
||||
// file with login credentials
|
||||
LoginsFile string `json:"authfile"`
|
||||
}
|
||||
|
||||
var DefaultNNTPConfig = NNTPServerConfig{
|
||||
AnonNNTP: false,
|
||||
Bind: "0.0.0.0:1119",
|
||||
Name: "nntp.server.tld",
|
||||
Article: &DefaultArticlePolicy,
|
||||
LoginsFile: "",
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package config
|
||||
|
||||
// proxy configuration
|
||||
type ProxyConfig struct {
|
||||
Type string `json:"type"`
|
||||
Addr string `json:"addr"`
|
||||
}
|
||||
|
||||
// default tor proxy
|
||||
var DefaultTorProxy = ProxyConfig{
|
||||
Type: "socks",
|
||||
Addr: "127.0.0.1:9050",
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package config
|
||||
|
||||
// settings for setting up ssl
|
||||
type SSLSettings struct {
|
||||
// path to ssl private key
|
||||
SSLKeyFile string `json:"key"`
|
||||
// path to ssl certificate signed by CA
|
||||
SSLCertFile string `json:"cert"`
|
||||
// domain name to use for ssl
|
||||
DomainName string `json:"fqdn"`
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package config
|
||||
|
||||
type StoreConfig struct {
|
||||
// path to article directory
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
var DefaultStoreConfig = StoreConfig{
|
||||
Path: "storage",
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
// configuration for a single web hook
|
||||
type WebhookConfig struct {
|
||||
// user provided name for this hook
|
||||
Name string `json:"name"`
|
||||
// callback URL for webhook
|
||||
URL string `json:"url"`
|
||||
// dialect to use when calling webhook
|
||||
Dialect string `json:"dialect"`
|
||||
}
|
||||
|
||||
var DefaultWebHookConfig = &WebhookConfig{
|
||||
Name: "vichan",
|
||||
Dialect: "vichan",
|
||||
URL: "http://localhost/webhook.php",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// nntpchan crypto package
|
||||
// wraps all external crypro libs
|
||||
//
|
||||
package crypto
|
||||
@@ -0,0 +1,8 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"github.com/dchest/blake256"
|
||||
)
|
||||
|
||||
// common hash function is blake2
|
||||
var Hash = blake256.New
|
||||
@@ -0,0 +1,77 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"hash"
|
||||
"nntpchan/lib/crypto/nacl"
|
||||
)
|
||||
|
||||
type fuckyNacl struct {
|
||||
k []byte
|
||||
hash hash.Hash
|
||||
}
|
||||
|
||||
func (fucky *fuckyNacl) Write(d []byte) (int, error) {
|
||||
return fucky.hash.Write(d)
|
||||
}
|
||||
|
||||
func (fucky *fuckyNacl) Sign() (s Signature) {
|
||||
h := fucky.hash.Sum(nil)
|
||||
if h == nil {
|
||||
panic("fuck.hash.Sum == nil")
|
||||
}
|
||||
|
||||
_, sec := nacl.SeedToKeyPair(fucky.k)
|
||||
sig := nacl.CryptoSignFucky(h, sec)
|
||||
if sig == nil {
|
||||
panic("fucky signer's call to nacl.CryptoSignFucky returned nil")
|
||||
}
|
||||
s = Signature(sig)
|
||||
fucky.resetState()
|
||||
return
|
||||
}
|
||||
|
||||
// reset inner state so we can reuse this fuckyNacl for another operation
|
||||
func (fucky *fuckyNacl) resetState() {
|
||||
fucky.hash = sha512.New()
|
||||
}
|
||||
|
||||
func (fucky *fuckyNacl) Verify(sig Signature) (valid bool) {
|
||||
h := fucky.hash.Sum(nil)
|
||||
if h == nil {
|
||||
panic("fucky.hash.Sum == nil")
|
||||
}
|
||||
valid = nacl.CryptoVerifyFucky(h, sig, fucky.k)
|
||||
fucky.resetState()
|
||||
return
|
||||
}
|
||||
|
||||
func createFucky(k []byte) *fuckyNacl {
|
||||
return &fuckyNacl{
|
||||
k: k,
|
||||
hash: sha512.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// create a standard signer given a secret key
|
||||
func CreateSigner(sk []byte) Signer {
|
||||
return createFucky(sk)
|
||||
}
|
||||
|
||||
// create a standard verifier given a public key
|
||||
func CreateVerifier(pk []byte) Verifer {
|
||||
return createFucky(pk)
|
||||
}
|
||||
|
||||
// get the public component given the secret key
|
||||
func ToPublic(sk []byte) (pk []byte) {
|
||||
pk, _ = nacl.SeedToKeyPair(sk)
|
||||
return
|
||||
}
|
||||
|
||||
// create a standard keypair
|
||||
func GenKeypair() (pk, sk []byte) {
|
||||
sk = RandBytes(32)
|
||||
pk, _ = nacl.SeedToKeyPair(sk)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package nacl
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"edwards25519"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
func CryptoVerifyFucky(h, sig, pk []byte) bool {
|
||||
pub := make(ed25519.PublicKey, ed25519.PublicKeySize)
|
||||
copy(pub, pk)
|
||||
return ed25519.Verify(pub, h, sig)
|
||||
}
|
||||
|
||||
func CryptoSignFucky(hash, sk []byte) []byte {
|
||||
sec := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
|
||||
copy(sec, sk)
|
||||
return ed25519.Sign(sec, hash)
|
||||
}
|
||||
|
||||
func SeedToKeyPair(seed []byte) (pk, sk []byte) {
|
||||
|
||||
h := sha512.Sum512(seed[0:32])
|
||||
sk = h[:]
|
||||
sk[0] &= 248
|
||||
sk[31] &= 63
|
||||
sk[31] |= 64
|
||||
// scalarmult magick shit
|
||||
pk = scalarBaseMult(sk[0:32])
|
||||
copy(sk[0:32], seed[0:32])
|
||||
copy(sk[32:64], pk[0:32])
|
||||
return
|
||||
}
|
||||
|
||||
func scalarBaseMult(sk []byte) (pk []byte) {
|
||||
var skey [32]byte
|
||||
var pkey [32]byte
|
||||
copy(skey[:], sk[0:32])
|
||||
var h edwards25519.ExtendedGroupElement
|
||||
edwards25519.GeScalarMultBase(&h, &skey)
|
||||
h.ToBytes(&pkey)
|
||||
pk = pkey[:]
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNaclToPublic(t *testing.T) {
|
||||
pk, sk := GenKeypair()
|
||||
t_pk := ToPublic(sk)
|
||||
if !bytes.Equal(pk, t_pk) {
|
||||
t.Logf("%q != %q", pk, t_pk)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaclSignVerify(t *testing.T) {
|
||||
var msg [1024]byte
|
||||
pk, sk := GenKeypair()
|
||||
io.ReadFull(rand.Reader, msg[:])
|
||||
|
||||
signer := CreateSigner(sk)
|
||||
signer.Write(msg[:])
|
||||
sig := signer.Sign()
|
||||
|
||||
verifier := CreateVerifier(pk)
|
||||
verifier.Write(msg[:])
|
||||
if !verifier.Verify(sig) {
|
||||
t.Logf("%q is invalid signature and is %dB long", sig, len(sig))
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
)
|
||||
|
||||
// generate random bytes
|
||||
func RandBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
io.ReadFull(rand.Reader, b)
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package crypto
|
||||
|
||||
import "io"
|
||||
|
||||
// a detached signature
|
||||
type Signature []byte
|
||||
|
||||
type SigEncoder interface {
|
||||
// encode a signature to an io.Writer
|
||||
// return error if one occurrened while writing out signature
|
||||
Encode(sig Signature, w io.Writer) error
|
||||
// encode a signature to a string
|
||||
EncodeString(sig Signature) string
|
||||
}
|
||||
|
||||
// a decoder of signatures
|
||||
type SigDecoder interface {
|
||||
// decode signature from io.Reader
|
||||
// reads all data until io.EOF
|
||||
// returns singaure or error if an error occured while reading
|
||||
Decode(r io.Reader) (Signature, error)
|
||||
// decode a signature from string
|
||||
// returns signature or error if an error ocurred while decoding
|
||||
DecodeString(str string) (Signature, error)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package crypto
|
||||
|
||||
import "io"
|
||||
|
||||
//
|
||||
// provides generic signing interface for producing detached signatures
|
||||
// call Write() to feed data to be signed, call Sign() to generate
|
||||
// a detached signature
|
||||
//
|
||||
type Signer interface {
|
||||
io.Writer
|
||||
// generate detached Signature from previously fed body via Write()
|
||||
Sign() Signature
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package crypto
|
||||
|
||||
import "io"
|
||||
|
||||
// provides generic signature
|
||||
// call Write() to feed in message body
|
||||
// once the entire body has been fed in via Write() call Verify() with detached
|
||||
// signature to verify the detached signature against the previously fed body
|
||||
type Verifer interface {
|
||||
io.Writer
|
||||
// verify detached signature from body previously fed via Write()
|
||||
// return true if the detached signature is valid given the body
|
||||
Verify(sig Signature) bool
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user