forked from iarv/nntpchan
Compare commits
612 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e2c1badcd | ||
|
|
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 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -19,23 +19,22 @@ webroot
|
||||
|
||||
# built binaries
|
||||
go
|
||||
srndv2
|
||||
./srndv2
|
||||
|
||||
# private key
|
||||
*.key
|
||||
*.txt
|
||||
|
||||
# certificates
|
||||
certs
|
||||
|
||||
|
||||
rebuild.sh
|
||||
vendor
|
||||
.gx
|
||||
|
||||
# generated js
|
||||
contrib/static/nntpchan.js
|
||||
contrib/static/js/nntpchan.js
|
||||
contrib/static/js/miner-js.js
|
||||
contrib/static/miner-js.js
|
||||
|
||||
#docs trash
|
||||
doc/.trash
|
||||
|
||||
34
.gxignore
34
.gxignore
@@ -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
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Jeff Becker
|
||||
Copyright (c) 2015-2017 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
|
||||
|
||||
40
Makefile
Normal file
40
Makefile
Normal file
@@ -0,0 +1,40 @@
|
||||
REPO=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
REPO_GOPATH=$(REPO)/go
|
||||
MINIFY=$(REPO_GOPATH)/bin/minify
|
||||
JS=$(REPO)/contrib/static/nntpchan.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
|
||||
SRND=$(REPO)/srndv2
|
||||
|
||||
all: clean build
|
||||
|
||||
build: js srnd
|
||||
|
||||
js: $(JS)
|
||||
|
||||
srnd: $(SRND)
|
||||
|
||||
$(MINIFY):
|
||||
GOPATH=$(REPO_GOPATH) go get -v github.com/tdewolff/minify/cmd/minify
|
||||
|
||||
js-deps: $(MINIFY)
|
||||
|
||||
$(JS): js-deps
|
||||
rm -f $(JS)
|
||||
for f in $(CONTRIB_JS)/*.js ; do $(MINIFY) --mime=text/javascript >> $(JS) < $$f ; done
|
||||
$(MINIFY) --mime=text/javascript >> $(JS) < $(REPO)/contrib/js/entry.js
|
||||
for f in $(LOCAL_JS)/*.js ; do $(MINIFY) --mime=text/javascript >> $(JS) < $$f ; done
|
||||
for f in $(VENDOR_JS)/*.js ; do $(MINIFY) --mime=text/javascript >> $(JS) < $$f ; done
|
||||
|
||||
|
||||
$(SRND):
|
||||
make -C $(SRND_DIR)
|
||||
cp $(SRND_DIR)/srndv2 $(SRND)
|
||||
|
||||
clean:
|
||||
rm -f $(SRND) $(JS)
|
||||
|
||||
distclean: clean
|
||||
rm -rf $(REPO_GOPATH)
|
||||
48
README.md
48
README.md
@@ -1,39 +1,53 @@
|
||||
NNTPChan
|
||||
========
|
||||
[NNTPChan](https://nntpchan.info)
|
||||
=================================
|
||||
|
||||
**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.
|
||||

|
||||
|
||||
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/)
|
||||
**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.
|
||||
|
||||
##Getting started
|
||||
## Getting started
|
||||
|
||||
[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.
|
||||
[This](doc) is a step-by-step guide for getting up-and-running with NNTPChan as well as documentation for developers who want to either work on NNTPChan directly or use NNTPChan in their aplications with the API.
|
||||
|
||||
##Bugs and issues
|
||||
## Bugs and issues
|
||||
|
||||
*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.
|
||||
|
||||
##Active NNTPChan nodes
|
||||
## Clients
|
||||
|
||||
Below is a list of known NNTPChan nodes:
|
||||
NNTP (confirmed working):
|
||||
|
||||
1. [2hu-ch.org](https://2hu-ch.org)
|
||||
2. [nsfl.tk](https://nsfl.tk)
|
||||
3. [i2p.rocks](https://i2p.rocks/ib/)
|
||||
* Thunderbird
|
||||
|
||||
##Support
|
||||
Web:
|
||||
|
||||
* [Yukko](https://github.com/faissaloo/Yukko): ncurses based nntpchan web ui reader
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
Need help? Join us on IRC.
|
||||
|
||||
1. [freenode: #nntpchan](https://webchat.freenode.net/?channels=#nntpchan)
|
||||
2. [rizon: #nntpchan](https://qchat.rizon.net/?channels=#nntpchan) - Most active
|
||||
|
||||
##Donations
|
||||
## History
|
||||
|
||||
Like this project? Why not help by funding it?
|
||||
* started in mid 2013 on anonet
|
||||
|
||||
This is a graph of the post flow of the `overchan.test` newsgroup over 4 years, quite a big network.
|
||||
|
||||
(thnx anon who made this btw)
|
||||
|
||||

|
||||
|
||||
## Donations
|
||||
|
||||
Like this project? Why not help by funding it? This address pays for the server that runs `2hu-ch.org`
|
||||
|
||||
Bitcoin: [15yuMzuueV8y5vPQQ39ZqQVz5Ey98DNrjE](bitcoin://15yuMzuueV8y5vPQQ39ZqQVz5Ey98DNrjE)
|
||||
|
||||
##Acknowledgements
|
||||
|
||||
* [Deavmi](deavmi.carteronline.net/~deavmi) - Making the documentation beautiful.
|
||||
## Acknowledgements
|
||||
|
||||
* [Deavmi](https://deavmi.carteronline.net/) - Making the documentation beautiful.
|
||||
|
||||
1
TODO.md
1
TODO.md
@@ -4,3 +4,4 @@
|
||||
* more alternative templates
|
||||
* javascript free mod panel
|
||||
* liveui
|
||||
* easier peering
|
||||
|
||||
71
build-js.sh
71
build-js.sh
@@ -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/js/miner-js.js
|
||||
rm ./miner_js.js.map
|
||||
|
||||
outfile=$PWD/contrib/static/js/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"
|
||||
92
build.sh
92
build.sh
@@ -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"
|
||||
5
contrib/backends/nntpchan-daemon/.gitignore
vendored
Normal file
5
contrib/backends/nntpchan-daemon/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
*.o
|
||||
nntpd
|
||||
nntpchan-tool
|
||||
test
|
||||
.gdb_history
|
||||
40
contrib/backends/nntpchan-daemon/Makefile
Normal file
40
contrib/backends/nntpchan-daemon/Makefile
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
EXE = nntpd
|
||||
|
||||
TOOL = nntpchan-tool
|
||||
|
||||
CXX = g++
|
||||
REPO=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
SRC_PATH = $(REPO)/src
|
||||
|
||||
SOURCES := $(wildcard $(SRC_PATH)/*.cpp)
|
||||
HEADERS := $(wildcard $(SRC_PATH)/*.hpp)
|
||||
OBJECTS := $(SOURCES:.cpp=.o)
|
||||
|
||||
PKGS := libuv libsodium
|
||||
|
||||
LD_FLAGS := $(shell pkg-config --libs $(PKGS))
|
||||
INC_FLAGS := $(shell pkg-config --cflags $(PKGS)) -I $(REPO)/src
|
||||
CXXFLAGS := -std=c++11 -Wall -Wextra $(INC_FLAGS) -g
|
||||
|
||||
|
||||
|
||||
all: $(EXE) $(TOOL)
|
||||
|
||||
$(EXE): $(OBJECTS)
|
||||
$(CXX) -o $(EXE) $(OBJECTS) $(CXXFLAGS) nntpd.cpp $(LD_FLAGS)
|
||||
|
||||
$(TOOL): $(OBJECTS)
|
||||
$(CXX) -o $(TOOL) $(OBJECTS) $(CXXFLAGS) tool.cpp $(LD_FLAGS)
|
||||
|
||||
build-test: $(OBJECTS)
|
||||
$(CXX) -o test $(OBJECTS) $(CXXFLAGS) test.cpp $(LD_FLAGS)
|
||||
|
||||
test: build-test
|
||||
./test
|
||||
|
||||
%.o: src/%.cpp
|
||||
$(CXX) $(CXXFLAGS) -c -o $@
|
||||
|
||||
clean:
|
||||
rm -f $(OBJECTS) $(EXE) $(TOOL) test
|
||||
2
contrib/backends/nntpchan-daemon/contrib/nntpchan.sh
Executable file
2
contrib/backends/nntpchan-daemon/contrib/nntpchan.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
6
contrib/backends/nntpchan-daemon/nntpchan.ini
Normal file
6
contrib/backends/nntpchan-daemon/nntpchan.ini
Normal file
@@ -0,0 +1,6 @@
|
||||
[nntp]
|
||||
bind = [::]:1199
|
||||
authdb=auth.txt
|
||||
|
||||
[storage]
|
||||
path = ./storage/
|
||||
103
contrib/backends/nntpchan-daemon/nntpd.cpp
Normal file
103
contrib/backends/nntpchan-daemon/nntpd.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "ini.hpp"
|
||||
|
||||
#include "storage.hpp"
|
||||
#include "nntp_server.hpp"
|
||||
#include "event.hpp"
|
||||
#include "exec_frontend.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
if (argc != 2) {
|
||||
std::cerr << "usage: " << argv[0] << " config.ini" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
nntpchan::Mainloop loop;
|
||||
|
||||
nntpchan::NNTPServer nntp(loop);
|
||||
|
||||
std::string fname(argv[1]);
|
||||
|
||||
std::ifstream i(fname);
|
||||
|
||||
if(i.is_open()) {
|
||||
INI::Parser conf(i);
|
||||
|
||||
std::vector<std::string> requiredSections = {"nntp", "storage"};
|
||||
|
||||
auto & level = conf.top();
|
||||
|
||||
for ( const auto & section : 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["storage"].values;
|
||||
|
||||
if (storeconf.find("path") == storeconf.end()) {
|
||||
std::cerr << "storage section does not have 'path' value" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
nntp.SetStoragePath(storeconf["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("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"]));
|
||||
} else {
|
||||
std::cerr << "unknown frontend type '" << ftype << "'" << std::endl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto & a = nntpconf["bind"];
|
||||
|
||||
try {
|
||||
nntp.Bind(a);
|
||||
} catch ( std::exception & ex ) {
|
||||
std::cerr << "failed to bind: " << ex.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
std::cerr << "run mainloop" << std::endl;
|
||||
loop.Run();
|
||||
} catch ( std::exception & ex ) {
|
||||
std::cerr << "exception in mainloop: " << ex.what() << std::endl;
|
||||
return 2;
|
||||
}
|
||||
|
||||
} else {
|
||||
std::cerr << "failed to open " << fname << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
234
contrib/backends/nntpchan-daemon/src/base64.cpp
Normal file
234
contrib/backends/nntpchan-daemon/src/base64.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
#include "base64.hpp"
|
||||
|
||||
|
||||
// taken from i2pd
|
||||
namespace i2p
|
||||
{
|
||||
namespace data
|
||||
{
|
||||
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', '+', '/'
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
static 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
|
||||
*
|
||||
*/
|
||||
static
|
||||
ssize_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;
|
||||
}
|
||||
|
||||
static size_t Base64EncodingBufferSize (const size_t input_size)
|
||||
{
|
||||
auto d = div (input_size, 3);
|
||||
if (d.rem) d.quot++;
|
||||
return 4*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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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()) == -1) return false;
|
||||
out.shrink_to_fit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
17
contrib/backends/nntpchan-daemon/src/base64.hpp
Normal file
17
contrib/backends/nntpchan-daemon/src/base64.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
21
contrib/backends/nntpchan-daemon/src/buffer.cpp
Normal file
21
contrib/backends/nntpchan-daemon/src/buffer.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "buffer.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
WriteBuffer::WriteBuffer(const char * b, const size_t s)
|
||||
{
|
||||
char * buf = new char[s];
|
||||
std::memcpy(buf, b, s);
|
||||
this->b = uv_buf_init(buf, s);
|
||||
w.data = this;
|
||||
};
|
||||
|
||||
WriteBuffer::WriteBuffer(const std::string & s) : WriteBuffer(s.c_str(), s.size()) {}
|
||||
|
||||
WriteBuffer::~WriteBuffer()
|
||||
{
|
||||
delete [] b.base;
|
||||
}
|
||||
}
|
||||
|
||||
19
contrib/backends/nntpchan-daemon/src/buffer.hpp
Normal file
19
contrib/backends/nntpchan-daemon/src/buffer.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef NNTPCHAN_BUFFER_HPP
|
||||
#define NNTPCHAN_BUFFER_HPP
|
||||
#include <uv.h>
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
struct WriteBuffer
|
||||
{
|
||||
uv_write_t w;
|
||||
uv_buf_t b;
|
||||
|
||||
WriteBuffer(const std::string & s);
|
||||
WriteBuffer(const char * b, const size_t s);
|
||||
~WriteBuffer();
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
9
contrib/backends/nntpchan-daemon/src/crypto.cpp
Normal file
9
contrib/backends/nntpchan-daemon/src/crypto.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#include "crypto.hpp"
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
void SHA512(const uint8_t * d, const std::size_t l, SHA512Digest & h)
|
||||
{
|
||||
crypto_hash(h.data(), d, l);
|
||||
}
|
||||
}
|
||||
15
contrib/backends/nntpchan-daemon/src/crypto.hpp
Normal file
15
contrib/backends/nntpchan-daemon/src/crypto.hpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifndef NNTPCHAN_CRYPTO_HPP
|
||||
#define NNTPCHAN_CRYPTO_HPP
|
||||
|
||||
#include <sodium/crypto_hash.h>
|
||||
#include <array>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
typedef std::array<uint8_t, crypto_hash_BYTES> SHA512Digest;
|
||||
|
||||
void SHA512(const uint8_t * d, std::size_t l, SHA512Digest & h);
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
27
contrib/backends/nntpchan-daemon/src/event.cpp
Normal file
27
contrib/backends/nntpchan-daemon/src/event.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "event.hpp"
|
||||
#include <cassert>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
Mainloop::Mainloop()
|
||||
{
|
||||
m_loop = uv_default_loop();
|
||||
assert(uv_loop_init(m_loop) == 0);
|
||||
}
|
||||
|
||||
Mainloop::~Mainloop()
|
||||
{
|
||||
uv_loop_close(m_loop);
|
||||
}
|
||||
|
||||
void Mainloop::Stop()
|
||||
{
|
||||
uv_stop(m_loop);
|
||||
}
|
||||
|
||||
void Mainloop::Run(uv_run_mode mode)
|
||||
{
|
||||
uv_run(m_loop, mode);
|
||||
}
|
||||
|
||||
}
|
||||
26
contrib/backends/nntpchan-daemon/src/event.hpp
Normal file
26
contrib/backends/nntpchan-daemon/src/event.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef NNTPCHAN_EVENT_HPP
|
||||
#define NNTPCHAN_EVENT_HPP
|
||||
#include <uv.h>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
class Mainloop
|
||||
{
|
||||
public:
|
||||
|
||||
Mainloop();
|
||||
~Mainloop();
|
||||
|
||||
operator uv_loop_t * () const { return m_loop; }
|
||||
|
||||
void Run(uv_run_mode mode = UV_RUN_DEFAULT);
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
|
||||
uv_loop_t * m_loop;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
57
contrib/backends/nntpchan-daemon/src/exec_frontend.cpp
Normal file
57
contrib/backends/nntpchan-daemon/src/exec_frontend.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "exec_frontend.hpp"
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
ExecFrontend::ExecFrontend(const std::string & fname) :
|
||||
m_exec(fname)
|
||||
{
|
||||
}
|
||||
|
||||
ExecFrontend::~ExecFrontend() {}
|
||||
|
||||
void ExecFrontend::ProcessNewMessage(const std::string & 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 = execvpe(m_exec.c_str(),(char * const *) cargs, environ);
|
||||
if ( r == -1 ) {
|
||||
std::cout << strerror(errno) << std::endl;
|
||||
exit( errno );
|
||||
} else
|
||||
exit(r);
|
||||
}
|
||||
return retcode;
|
||||
}
|
||||
}
|
||||
30
contrib/backends/nntpchan-daemon/src/exec_frontend.hpp
Normal file
30
contrib/backends/nntpchan-daemon/src/exec_frontend.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#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);
|
||||
|
||||
~ExecFrontend();
|
||||
|
||||
void ProcessNewMessage(const std::string & fpath);
|
||||
bool AcceptsNewsgroup(const std::string & newsgroup);
|
||||
bool AcceptsMessage(const std::string & msgid);
|
||||
|
||||
private:
|
||||
|
||||
int Exec(std::deque<std::string> args);
|
||||
|
||||
private:
|
||||
std::string m_exec;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
25
contrib/backends/nntpchan-daemon/src/frontend.hpp
Normal file
25
contrib/backends/nntpchan-daemon/src/frontend.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef NNTPCHAN_FRONTEND_HPP
|
||||
#define NNTPCHAN_FRONTEND_HPP
|
||||
#include <string>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
/** @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 std::string & 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;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
186
contrib/backends/nntpchan-daemon/src/ini.hpp
Normal file
186
contrib/backends/nntpchan-daemon/src/ini.hpp
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 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 <map>
|
||||
#include <list>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
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
|
||||
|
||||
44
contrib/backends/nntpchan-daemon/src/line.cpp
Normal file
44
contrib/backends/nntpchan-daemon/src/line.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "line.hpp"
|
||||
#include <iostream>
|
||||
|
||||
namespace nntpchan {
|
||||
|
||||
void LineReader::OnData(const char * d, ssize_t l)
|
||||
{
|
||||
if(l <= 0) return;
|
||||
std::size_t idx = 0;
|
||||
while(l-- > 0) {
|
||||
char c = d[idx++];
|
||||
if(c == '\n') {
|
||||
OnLine(d, idx-1);
|
||||
d += idx;
|
||||
} else if (c == '\r' && d[idx] == '\n') {
|
||||
OnLine(d, idx-1);
|
||||
d += idx + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LineReader::OnLine(const char *d, const size_t l)
|
||||
{
|
||||
std::string line(d, l);
|
||||
HandleLine(line);
|
||||
}
|
||||
|
||||
bool LineReader::HasNextLine()
|
||||
{
|
||||
return m_sendlines.size() > 0;
|
||||
}
|
||||
|
||||
std::string LineReader::GetNextLine()
|
||||
{
|
||||
std::string line = m_sendlines[0];
|
||||
m_sendlines.pop_front();
|
||||
return line;
|
||||
}
|
||||
|
||||
void LineReader::QueueLine(const std::string & line)
|
||||
{
|
||||
m_sendlines.push_back(line);
|
||||
}
|
||||
}
|
||||
35
contrib/backends/nntpchan-daemon/src/line.hpp
Normal file
35
contrib/backends/nntpchan-daemon/src/line.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef NNTPCHAN_LINE_HPP
|
||||
#define NNTPCHAN_LINE_HPP
|
||||
#include <string>
|
||||
#include <deque>
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
/** @brief a buffered line reader */
|
||||
class LineReader
|
||||
{
|
||||
public:
|
||||
|
||||
|
||||
/** @brief queue inbound data from connection */
|
||||
void OnData(const char * data, ssize_t s);
|
||||
|
||||
/** @brief do we have line to send to the client? */
|
||||
bool HasNextLine();
|
||||
/** @brief get the next line to send to the client, does not check if it exists */
|
||||
std::string GetNextLine();
|
||||
|
||||
protected:
|
||||
/** @brief handle a line from the client */
|
||||
virtual void HandleLine(const std::string & line) = 0;
|
||||
/** @brief queue the next line to send to the client */
|
||||
void QueueLine(const std::string & line);
|
||||
|
||||
private:
|
||||
void OnLine(const char * d, const size_t l);
|
||||
// lines to send
|
||||
std::deque<std::string> m_sendlines;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
29
contrib/backends/nntpchan-daemon/src/message.cpp
Normal file
29
contrib/backends/nntpchan-daemon/src/message.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#include "message.hpp"
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
bool IsValidMessageID(const MessageID & msgid)
|
||||
{
|
||||
auto itr = msgid.begin();
|
||||
auto end = msgid.end();
|
||||
--end;
|
||||
if (*itr != '<') return false;
|
||||
if (*end != '>') return false;
|
||||
bool atfound = false;
|
||||
while(itr != end) {
|
||||
auto c = *itr;
|
||||
++itr;
|
||||
if(atfound && c == '@') return false;
|
||||
if(c == '@') {
|
||||
atfound = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '$' || c == '_' || c == '-') continue;
|
||||
if (c > '0' && c < '9') continue;
|
||||
if (c > 'A' && c < 'Z') continue;
|
||||
if (c > 'a' && c < 'z') continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
36
contrib/backends/nntpchan-daemon/src/message.hpp
Normal file
36
contrib/backends/nntpchan-daemon/src/message.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef NNTPCHAN_MESSAGE_HPP
|
||||
#define NNTPCHAN_MESSAGE_HPP
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
typedef std::string MessageID;
|
||||
|
||||
bool IsValidMessageID(const MessageID & msgid);
|
||||
|
||||
typedef std::pair<std::string, std::string> MessageHeader;
|
||||
|
||||
typedef std::map<std::string, std::string> MIMEPartHeader;
|
||||
|
||||
typedef std::function<bool(const MessageHeader &)> MessageHeaderFilter;
|
||||
|
||||
typedef std::function<bool(const MIMEPartHeader &)> MIMEPartFilter;
|
||||
|
||||
/**
|
||||
read MIME message from i,
|
||||
filter each header with h,
|
||||
filter each part with p,
|
||||
store result in o
|
||||
|
||||
return true if we read the whole message, return false if there is remaining
|
||||
*/
|
||||
bool StoreMIMEMessage(std::istream & i, MessageHeaderFilter h, MIMEPartHeader p, std::ostream & o);
|
||||
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
44
contrib/backends/nntpchan-daemon/src/net.cpp
Normal file
44
contrib/backends/nntpchan-daemon/src/net.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "net.hpp"
|
||||
#include <uv.h>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <cstring>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
std::string NetAddr::to_string()
|
||||
{
|
||||
std::string str("invalid");
|
||||
const size_t s = 128;
|
||||
char * buff = new char[s];
|
||||
if(uv_ip6_name(&addr, buff, s) == 0) {
|
||||
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);
|
||||
uv_ip6_addr(a.c_str(), port, &saddr.addr);
|
||||
return saddr;
|
||||
}
|
||||
}
|
||||
23
contrib/backends/nntpchan-daemon/src/net.hpp
Normal file
23
contrib/backends/nntpchan-daemon/src/net.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef NNTPCHAN_NET_HPP
|
||||
#define NNTPCHAN_NET_HPP
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <netinet/in.h>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
98
contrib/backends/nntpchan-daemon/src/nntp_auth.cpp
Normal file
98
contrib/backends/nntpchan-daemon/src/nntp_auth.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#include "nntp_auth.hpp"
|
||||
#include "crypto.hpp"
|
||||
#include "base64.hpp"
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
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);
|
||||
OnData(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;
|
||||
}
|
||||
}
|
||||
|
||||
57
contrib/backends/nntpchan-daemon/src/nntp_auth.hpp
Normal file
57
contrib/backends/nntpchan-daemon/src/nntp_auth.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef NNTPCHAN_NNTP_AUTH_HPP
|
||||
#define NNTPCHAN_NNTP_AUTH_HPP
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include "line.hpp"
|
||||
|
||||
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() {}
|
||||
};
|
||||
|
||||
/** @brief nntp credential db using hashed+salted passwords */
|
||||
class HashedCredDB : public NNTPCredentialDB, public LineReader
|
||||
{
|
||||
public:
|
||||
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
|
||||
117
contrib/backends/nntpchan-daemon/src/nntp_handler.cpp
Normal file
117
contrib/backends/nntpchan-daemon/src/nntp_handler.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#include "nntp_handler.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
NNTPServerHandler::NNTPServerHandler(const std::string & storage) :
|
||||
m_auth(nullptr),
|
||||
m_store(storage),
|
||||
m_authed(false),
|
||||
m_state(eStateReadCommand)
|
||||
{
|
||||
}
|
||||
|
||||
NNTPServerHandler::~NNTPServerHandler()
|
||||
{
|
||||
if(m_auth) delete m_auth;
|
||||
}
|
||||
|
||||
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(std::string(part));
|
||||
}
|
||||
if(command.size())
|
||||
HandleCommand(command);
|
||||
else
|
||||
QueueLine("501 Syntax error");
|
||||
}
|
||||
}
|
||||
|
||||
void NNTPServerHandler::HandleCommand(const std::deque<std::string> & command)
|
||||
{
|
||||
auto cmd = command[0];
|
||||
std::transform(cmd.begin(), cmd.end(), cmd.begin(),
|
||||
[](unsigned char ch) { return std::toupper(ch); });
|
||||
std::size_t cmdlen = command.size();
|
||||
std::cerr << "handle command [" << cmd << "] " << (int) cmdlen << std::endl;
|
||||
if (cmd == "QUIT") {
|
||||
Quit();
|
||||
return;
|
||||
} else if (cmd == "MODE" ) {
|
||||
if(cmdlen == 1) {
|
||||
// set mode
|
||||
SwitchMode(command[1]);
|
||||
} else if(cmdlen) {
|
||||
// too many arguments
|
||||
} else {
|
||||
// get mode
|
||||
}
|
||||
|
||||
} else {
|
||||
// unknown command
|
||||
QueueLine("500 Unknown Command");
|
||||
}
|
||||
}
|
||||
|
||||
void NNTPServerHandler::SwitchMode(const std::string & mode)
|
||||
{
|
||||
if (mode == "READER") {
|
||||
m_mode = mode;
|
||||
if(PostingAllowed()) {
|
||||
QueueLine("200 Posting is permitted yo");
|
||||
} else {
|
||||
QueueLine("201 Posting is not permitted yo");
|
||||
}
|
||||
} else if (mode == "STREAM") {
|
||||
m_mode = mode;
|
||||
if (PostingAllowed()) {
|
||||
QueueLine("203 Streaming enabled");
|
||||
} else {
|
||||
QueueLine("483 Streaming Denied");
|
||||
}
|
||||
} else {
|
||||
// unknown mode
|
||||
QueueLine("500 Unknown mode");
|
||||
}
|
||||
}
|
||||
|
||||
void NNTPServerHandler::Quit()
|
||||
{
|
||||
std::cerr << "quitting" << std::endl;
|
||||
m_state = eStateQuit;
|
||||
QueueLine("205 quitting");
|
||||
}
|
||||
|
||||
bool NNTPServerHandler::Done()
|
||||
{
|
||||
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(NNTPCredentialDB *creds)
|
||||
{
|
||||
if(m_auth) delete m_auth;
|
||||
m_auth = creds;
|
||||
}
|
||||
}
|
||||
53
contrib/backends/nntpchan-daemon/src/nntp_handler.hpp
Normal file
53
contrib/backends/nntpchan-daemon/src/nntp_handler.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#ifndef NNTPCHAN_NNTP_HANDLER_HPP
|
||||
#define NNTPCHAN_NNTP_HANDLER_HPP
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include "line.hpp"
|
||||
#include "nntp_auth.hpp"
|
||||
#include "storage.hpp"
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
class NNTPServerHandler : public LineReader
|
||||
{
|
||||
public:
|
||||
NNTPServerHandler(const std::string & storage);
|
||||
~NNTPServerHandler();
|
||||
|
||||
bool Done();
|
||||
|
||||
void SetAuth(NNTPCredentialDB * creds);
|
||||
|
||||
void Greet();
|
||||
|
||||
protected:
|
||||
void HandleLine(const std::string & line);
|
||||
void HandleCommand(const std::deque<std::string> & command);
|
||||
private:
|
||||
|
||||
enum State {
|
||||
eStateReadCommand,
|
||||
eStateStoreArticle,
|
||||
eStateQuit
|
||||
};
|
||||
|
||||
private:
|
||||
// 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:
|
||||
NNTPCredentialDB * m_auth;
|
||||
ArticleStorage m_store;
|
||||
std::string m_mode;
|
||||
bool m_authed;
|
||||
State m_state;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
149
contrib/backends/nntpchan-daemon/src/nntp_server.cpp
Normal file
149
contrib/backends/nntpchan-daemon/src/nntp_server.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include "buffer.hpp"
|
||||
#include "nntp_server.hpp"
|
||||
#include "nntp_auth.hpp"
|
||||
#include "net.hpp"
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
NNTPServer::NNTPServer(uv_loop_t * loop)
|
||||
{
|
||||
uv_tcp_init(loop, &m_server);
|
||||
m_loop = loop;
|
||||
m_server.data = this;
|
||||
}
|
||||
|
||||
NNTPServer::~NNTPServer()
|
||||
{
|
||||
if (m_frontend) delete m_frontend;
|
||||
}
|
||||
|
||||
void NNTPServer::Close()
|
||||
{
|
||||
uv_close((uv_handle_t*)&m_server, [](uv_handle_t * s) {
|
||||
NNTPServer * self = (NNTPServer*)s->data;
|
||||
if (self) delete self;
|
||||
s->data = nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
void NNTPServer::Bind(const std::string & addr)
|
||||
{
|
||||
auto saddr = ParseAddr(addr);
|
||||
assert(uv_tcp_bind(*this, saddr, 0) == 0);
|
||||
std::cerr << "nntp server bound to " << saddr.to_string() << std::endl;
|
||||
auto cb = [] (uv_stream_t * s, int status) {
|
||||
NNTPServer * self = (NNTPServer *) s->data;
|
||||
self->OnAccept(s, status);
|
||||
};
|
||||
|
||||
assert(uv_listen(*this, 5, cb) == 0);
|
||||
}
|
||||
|
||||
void NNTPServer::OnAccept(uv_stream_t * s, int status)
|
||||
{
|
||||
if(status < 0) {
|
||||
std::cerr << "nntp server OnAccept fail: " << uv_strerror(status) << std::endl;
|
||||
return;
|
||||
}
|
||||
NNTPCredentialDB * creds = nullptr;
|
||||
|
||||
std::ifstream i;
|
||||
i.open(m_logindbpath);
|
||||
if(i.is_open()) creds = new HashedFileDB(m_logindbpath);
|
||||
|
||||
NNTPServerConn * conn = new NNTPServerConn(m_loop, s, m_storagePath, creds);
|
||||
conn->Greet();
|
||||
}
|
||||
|
||||
|
||||
void NNTPServer::SetLoginDB(const std::string path)
|
||||
{
|
||||
m_logindbpath = path;
|
||||
}
|
||||
|
||||
|
||||
void NNTPServer::SetStoragePath(const std::string & path)
|
||||
{
|
||||
m_storagePath = path;
|
||||
}
|
||||
|
||||
void NNTPServer::SetFrontend(Frontend * f)
|
||||
{
|
||||
if(m_frontend) delete m_frontend;
|
||||
m_frontend = f;
|
||||
}
|
||||
|
||||
NNTPServerConn::NNTPServerConn(uv_loop_t * l, uv_stream_t * s, const std::string & storage, NNTPCredentialDB * creds) :
|
||||
m_handler(storage)
|
||||
{
|
||||
m_handler.SetAuth(creds);
|
||||
uv_tcp_init(l, &m_conn);
|
||||
m_conn.data = this;
|
||||
uv_accept(s, (uv_stream_t*) &m_conn);
|
||||
uv_read_start((uv_stream_t*) &m_conn, [] (uv_handle_t * h, size_t s, uv_buf_t * b) {
|
||||
NNTPServerConn * self = (NNTPServerConn*) h->data;
|
||||
if(self == nullptr) return;
|
||||
b->base = self->m_readbuff;
|
||||
if (s > sizeof(self->m_readbuff))
|
||||
b->len = sizeof(self->m_readbuff);
|
||||
else
|
||||
b->len = s;
|
||||
}, [] (uv_stream_t * s, ssize_t nread, const uv_buf_t * b) {
|
||||
NNTPServerConn * self = (NNTPServerConn*) s->data;
|
||||
if(self == nullptr) return;
|
||||
if(nread > 0) {
|
||||
self->m_handler.OnData(b->base, nread);
|
||||
self->SendNextReply();
|
||||
if(self->m_handler.Done())
|
||||
self->Close();
|
||||
} else {
|
||||
if (nread != UV_EOF) {
|
||||
std::cerr << "error in nntp server conn alloc: ";
|
||||
std::cerr << uv_strerror(nread);
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
// got eof or error
|
||||
self->Close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void NNTPServerConn::SendNextReply()
|
||||
{
|
||||
if(m_handler.HasNextLine()) {
|
||||
auto line = m_handler.GetNextLine();
|
||||
SendString(line+"\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void NNTPServerConn::Greet()
|
||||
{
|
||||
m_handler.Greet();
|
||||
SendNextReply();
|
||||
}
|
||||
|
||||
void NNTPServerConn::SendString(const std::string & str)
|
||||
{
|
||||
WriteBuffer * b = new WriteBuffer(str);
|
||||
uv_write(&b->w, *this, &b->b, 1, [](uv_write_t * w, int status) {
|
||||
(void) status;
|
||||
WriteBuffer * wb = (WriteBuffer *) w->data;
|
||||
if(wb)
|
||||
delete wb;
|
||||
});
|
||||
}
|
||||
|
||||
void NNTPServerConn::Close()
|
||||
{
|
||||
uv_close((uv_handle_t*)&m_conn, [] (uv_handle_t * s) {
|
||||
NNTPServerConn * self = (NNTPServerConn*) s->data;
|
||||
if(self)
|
||||
delete self;
|
||||
s->data = nullptr;
|
||||
});
|
||||
}
|
||||
}
|
||||
80
contrib/backends/nntpchan-daemon/src/nntp_server.hpp
Normal file
80
contrib/backends/nntpchan-daemon/src/nntp_server.hpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#ifndef NNTPCHAN_NNTP_SERVER_HPP
|
||||
#define NNTPCHAN_NNTP_SERVER_HPP
|
||||
#include <uv.h>
|
||||
#include <string>
|
||||
#include <deque>
|
||||
#include "storage.hpp"
|
||||
#include "frontend.hpp"
|
||||
#include "nntp_auth.hpp"
|
||||
#include "nntp_handler.hpp"
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
|
||||
class NNTPServerConn;
|
||||
|
||||
class NNTPServer
|
||||
{
|
||||
public:
|
||||
NNTPServer(uv_loop_t * loop);
|
||||
~NNTPServer();
|
||||
|
||||
void Bind(const std::string & addr);
|
||||
|
||||
void OnAccept(uv_stream_t * s, int status);
|
||||
|
||||
void SetStoragePath(const std::string & path);
|
||||
|
||||
void SetLoginDB(const std::string path);
|
||||
|
||||
void SetFrontend(Frontend * f);
|
||||
|
||||
void Close();
|
||||
|
||||
private:
|
||||
|
||||
operator uv_handle_t * () { return (uv_handle_t*) &m_server; }
|
||||
operator uv_tcp_t * () { return &m_server; }
|
||||
operator uv_stream_t * () { return (uv_stream_t *) &m_server; }
|
||||
|
||||
uv_tcp_t m_server;
|
||||
uv_loop_t * m_loop;
|
||||
|
||||
std::deque<NNTPServerConn *> m_conns;
|
||||
|
||||
std::string m_logindbpath;
|
||||
std::string m_storagePath;
|
||||
|
||||
Frontend * m_frontend;
|
||||
|
||||
};
|
||||
|
||||
class NNTPServerConn
|
||||
{
|
||||
public:
|
||||
NNTPServerConn(uv_loop_t * l, uv_stream_t * s, const std::string & storage, NNTPCredentialDB * creds);
|
||||
/** @brief close connection, this connection cannot be used after calling this */
|
||||
void Close();
|
||||
|
||||
/** @brief send next queued reply */
|
||||
void SendNextReply();
|
||||
|
||||
void Greet();
|
||||
|
||||
private:
|
||||
|
||||
void SendString(const std::string & line);
|
||||
|
||||
|
||||
operator uv_stream_t * () { return (uv_stream_t *) &m_conn; }
|
||||
|
||||
uv_tcp_t m_conn;
|
||||
|
||||
NNTPServerHandler m_handler;
|
||||
|
||||
char m_readbuff[1028];
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
46
contrib/backends/nntpchan-daemon/src/storage.cpp
Normal file
46
contrib/backends/nntpchan-daemon/src/storage.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "storage.hpp"
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sstream>
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
ArticleStorage::ArticleStorage()
|
||||
{
|
||||
}
|
||||
|
||||
ArticleStorage::ArticleStorage(const std::string & fpath) {
|
||||
SetPath(fpath);
|
||||
}
|
||||
|
||||
ArticleStorage::~ArticleStorage()
|
||||
{
|
||||
}
|
||||
|
||||
void ArticleStorage::SetPath(const std::string & fpath)
|
||||
{
|
||||
basedir = fpath;
|
||||
// quiet fail
|
||||
// TODO: check for errors
|
||||
mkdir(basedir.c_str(), 0700);
|
||||
}
|
||||
|
||||
bool ArticleStorage::Accept(const MessageID & msgid)
|
||||
{
|
||||
if (!IsValidMessageID(msgid)) return false;
|
||||
std::stringstream ss;
|
||||
ss << basedir << GetPathSep() << msgid;
|
||||
auto s = ss.str();
|
||||
FILE * f = fopen(s.c_str(), "r");
|
||||
if ( f == nullptr) return errno == ENOENT;
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
|
||||
char ArticleStorage::GetPathSep()
|
||||
{
|
||||
return '/';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
36
contrib/backends/nntpchan-daemon/src/storage.hpp
Normal file
36
contrib/backends/nntpchan-daemon/src/storage.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef NNTPCHAN_STORAGE_HPP
|
||||
#define NNTPCHAN_STORAGE_HPP
|
||||
|
||||
#include <string>
|
||||
#include "message.hpp"
|
||||
|
||||
namespace nntpchan
|
||||
{
|
||||
class ArticleStorage
|
||||
{
|
||||
public:
|
||||
ArticleStorage();
|
||||
ArticleStorage(const std::string & fpath);
|
||||
~ArticleStorage();
|
||||
|
||||
void SetPath(const std::string & fpath);
|
||||
|
||||
std::ostream & OpenWrite(const MessageID & msgid);
|
||||
std::istream & OpenRead(const MessageID & msgid);
|
||||
|
||||
/**
|
||||
return true if we should accept a new message give its message id
|
||||
*/
|
||||
bool Accept(const MessageID & msgid);
|
||||
|
||||
private:
|
||||
|
||||
static char GetPathSep();
|
||||
|
||||
std::string basedir;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
13
contrib/backends/nntpchan-daemon/test.cpp
Normal file
13
contrib/backends/nntpchan-daemon/test.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "exec_frontend.hpp"
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
|
||||
|
||||
|
||||
int main(int , char * [])
|
||||
{
|
||||
nntpchan::Frontend * f = new nntpchan::ExecFrontend("./contrib/nntpchan.sh");
|
||||
assert(f->AcceptsMessage("<test@server>"));
|
||||
assert(f->AcceptsNewsgroup("overchan.test"));
|
||||
std::cout << "all good" << std::endl;
|
||||
}
|
||||
91
contrib/backends/nntpchan-daemon/tool.cpp
Normal file
91
contrib/backends/nntpchan-daemon/tool.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "base64.hpp"
|
||||
#include "crypto.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <sodium.h>
|
||||
|
||||
static void print_help(const std::string & exename)
|
||||
{
|
||||
std::cout << "usage: " << exename << " [help|gencred|checkcred]" << std::endl;
|
||||
}
|
||||
|
||||
static void print_long_help() {
|
||||
|
||||
}
|
||||
|
||||
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 1;
|
||||
}
|
||||
std::string cmd(argv[1]);
|
||||
if (cmd == "help") {
|
||||
print_long_help();
|
||||
return 0;
|
||||
}
|
||||
if (cmd == "gencred") {
|
||||
if(argc == 4) {
|
||||
gen_passwd(argv[2], argv[3]);
|
||||
return 0;
|
||||
} else {
|
||||
std::cout << "usage: " << argv[0] << " passwd username password" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if(cmd == "checkcred" ) {
|
||||
std::string cred;
|
||||
std::cout << "credential: " ;
|
||||
if(!std::getline(std::cin, cred)) {
|
||||
return 1;
|
||||
}
|
||||
std::string passwd;
|
||||
std::cout << "password: ";
|
||||
if(!std::getline(std::cin, passwd)) {
|
||||
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;
|
||||
}
|
||||
11
contrib/backends/nntpchand/Makefile
Normal file
11
contrib/backends/nntpchand/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
REPO=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
all: clean build
|
||||
|
||||
build: nntpchand
|
||||
|
||||
nntpchand:
|
||||
GOPATH=$(REPO) go build -v
|
||||
|
||||
clean:
|
||||
GOPATH=$(REPO) go clean -v
|
||||
9
contrib/backends/nntpchand/main.go
Normal file
9
contrib/backends/nntpchand/main.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"nntpchan/cmd/nntpchan"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nntpchan.Main()
|
||||
}
|
||||
160
contrib/backends/nntpchand/src/nntpchan/cmd/nntpchan/main.go
Normal file
160
contrib/backends/nntpchand/src/nntpchan/cmd/nntpchan/main.go
Normal file
@@ -0,0 +1,160 @@
|
||||
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 db database.Database
|
||||
for _, fconf := range conf.Frontends {
|
||||
var f frontend.Frontend
|
||||
f, err = frontend.NewHTTPFrontend(fconf, db)
|
||||
if err == nil {
|
||||
go f.Serve()
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
} 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"
|
||||
"github.com/majestrate/srndv2/lib/config"
|
||||
"github.com/majestrate/srndv2/lib/nntp"
|
||||
"github.com/majestrate/srndv2/lib/store"
|
||||
"net"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
4
contrib/backends/nntpchand/src/nntpchan/lib/admin/doc.go
Normal file
4
contrib/backends/nntpchand/src/nntpchan/lib/admin/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
//
|
||||
// server admin panel
|
||||
//
|
||||
package admin
|
||||
16
contrib/backends/nntpchand/src/nntpchan/lib/admin/server.go
Normal file
16
contrib/backends/nntpchand/src/nntpchan/lib/admin/server.go
Normal file
@@ -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{}
|
||||
}
|
||||
10
contrib/backends/nntpchand/src/nntpchan/lib/api/api.go
Normal file
10
contrib/backends/nntpchand/src/nntpchan/lib/api/api.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"nntpchan/lib/model"
|
||||
)
|
||||
|
||||
// json api
|
||||
type API interface {
|
||||
MakePost(p model.Post)
|
||||
}
|
||||
2
contrib/backends/nntpchand/src/nntpchan/lib/api/doc.go
Normal file
2
contrib/backends/nntpchand/src/nntpchan/lib/api/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// json api
|
||||
package api
|
||||
20
contrib/backends/nntpchand/src/nntpchan/lib/api/server.go
Normal file
20
contrib/backends/nntpchand/src/nntpchan/lib/api/server.go
Normal file
@@ -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,
|
||||
}
|
||||
13
contrib/backends/nntpchand/src/nntpchan/lib/config/cache.go
Normal file
13
contrib/backends/nntpchand/src/nntpchan/lib/config/cache.go
Normal file
@@ -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"`
|
||||
}
|
||||
87
contrib/backends/nntpchand/src/nntpchan/lib/config/config.go
Normal file
87
contrib/backends/nntpchand/src/nntpchan/lib/config/config.go
Normal file
@@ -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
|
||||
33
contrib/backends/nntpchand/src/nntpchan/lib/config/feed.go
Normal file
33
contrib/backends/nntpchand/src/nntpchan/lib/config/feed.go
Normal file
@@ -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,21 @@
|
||||
package config
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// default Frontend Configuration
|
||||
var DefaultFrontendConfig = FrontendConfig{
|
||||
BindAddr: "127.0.0.1:18888",
|
||||
Static: "./files/static/",
|
||||
Middleware: &DefaultMiddlewareConfig,
|
||||
}
|
||||
15
contrib/backends/nntpchand/src/nntpchan/lib/config/hook.go
Normal file
15
contrib/backends/nntpchand/src/nntpchan/lib/config/hook.go
Normal file
@@ -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,14 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
var DefaultMiddlewareConfig = MiddlewareConfig{
|
||||
Type: "overchan",
|
||||
Templates: "./files/templates/overchan/",
|
||||
}
|
||||
24
contrib/backends/nntpchand/src/nntpchan/lib/config/nntp.go
Normal file
24
contrib/backends/nntpchand/src/nntpchan/lib/config/nntp.go
Normal file
@@ -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: "",
|
||||
}
|
||||
13
contrib/backends/nntpchand/src/nntpchan/lib/config/proxy.go
Normal file
13
contrib/backends/nntpchand/src/nntpchan/lib/config/proxy.go
Normal file
@@ -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",
|
||||
}
|
||||
11
contrib/backends/nntpchand/src/nntpchan/lib/config/ssl.go
Normal file
11
contrib/backends/nntpchand/src/nntpchan/lib/config/ssl.go
Normal file
@@ -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"`
|
||||
}
|
||||
10
contrib/backends/nntpchand/src/nntpchan/lib/config/store.go
Normal file
10
contrib/backends/nntpchand/src/nntpchan/lib/config/store.go
Normal file
@@ -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
|
||||
82
contrib/backends/nntpchand/src/nntpchan/lib/crypto/nacl.go
Normal file
82
contrib/backends/nntpchand/src/nntpchan/lib/crypto/nacl.go
Normal file
@@ -0,0 +1,82 @@
|
||||
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")
|
||||
}
|
||||
kp := nacl.LoadSignKey(fucky.k)
|
||||
defer kp.Free()
|
||||
sk := kp.Secret()
|
||||
sig := nacl.CryptoSignFucky(h, sk)
|
||||
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("fuck.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) {
|
||||
kp := nacl.LoadSignKey(sk)
|
||||
defer kp.Free()
|
||||
pk = kp.Public()
|
||||
return
|
||||
}
|
||||
|
||||
// create a standard keypair
|
||||
func GenKeypair() (pk, sk []byte) {
|
||||
kp := nacl.GenSignKeypair()
|
||||
defer kp.Free()
|
||||
pk = kp.Public()
|
||||
sk = kp.Seed()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package nacl
|
||||
|
||||
// #cgo freebsd CFLAGS: -I/usr/local/include
|
||||
// #cgo freebsd LDFLAGS: -L/usr/local/lib
|
||||
// #cgo LDFLAGS: -lsodium
|
||||
// #include <sodium.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// encrypts a message to a user given their public key is known
|
||||
// returns an encrypted box
|
||||
func CryptoBox(msg, nounce, pk, sk []byte) ([]byte, error) {
|
||||
msgbuff := NewBuffer(msg)
|
||||
defer msgbuff.Free()
|
||||
|
||||
// check sizes
|
||||
if len(pk) != int(C.crypto_box_publickeybytes()) {
|
||||
err := errors.New("len(pk) != crypto_box_publickey_bytes")
|
||||
return nil, err
|
||||
}
|
||||
if len(sk) != int(C.crypto_box_secretkeybytes()) {
|
||||
err := errors.New("len(sk) != crypto_box_secretkey_bytes")
|
||||
return nil, err
|
||||
}
|
||||
if len(nounce) != int(C.crypto_box_macbytes()) {
|
||||
err := errors.New("len(nounce) != crypto_box_macbytes()")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkbuff := NewBuffer(pk)
|
||||
defer pkbuff.Free()
|
||||
skbuff := NewBuffer(sk)
|
||||
defer skbuff.Free()
|
||||
nouncebuff := NewBuffer(nounce)
|
||||
defer nouncebuff.Free()
|
||||
|
||||
resultbuff := malloc(msgbuff.size + nouncebuff.size)
|
||||
defer resultbuff.Free()
|
||||
res := C.crypto_box_easy(resultbuff.uchar(), msgbuff.uchar(), C.ulonglong(msgbuff.size), nouncebuff.uchar(), pkbuff.uchar(), skbuff.uchar())
|
||||
if res != 0 {
|
||||
err := errors.New("crypto_box_easy failed")
|
||||
return nil, err
|
||||
}
|
||||
return resultbuff.Bytes(), nil
|
||||
}
|
||||
|
||||
// open an encrypted box
|
||||
func CryptoBoxOpen(box, nounce, sk, pk []byte) ([]byte, error) {
|
||||
boxbuff := NewBuffer(box)
|
||||
defer boxbuff.Free()
|
||||
|
||||
// check sizes
|
||||
if len(pk) != int(C.crypto_box_publickeybytes()) {
|
||||
err := errors.New("len(pk) != crypto_box_publickey_bytes")
|
||||
return nil, err
|
||||
}
|
||||
if len(sk) != int(C.crypto_box_secretkeybytes()) {
|
||||
err := errors.New("len(sk) != crypto_box_secretkey_bytes")
|
||||
return nil, err
|
||||
}
|
||||
if len(nounce) != int(C.crypto_box_macbytes()) {
|
||||
err := errors.New("len(nounce) != crypto_box_macbytes()")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkbuff := NewBuffer(pk)
|
||||
defer pkbuff.Free()
|
||||
skbuff := NewBuffer(sk)
|
||||
defer skbuff.Free()
|
||||
nouncebuff := NewBuffer(nounce)
|
||||
defer nouncebuff.Free()
|
||||
resultbuff := malloc(boxbuff.size - nouncebuff.size)
|
||||
defer resultbuff.Free()
|
||||
|
||||
// decrypt
|
||||
res := C.crypto_box_open_easy(resultbuff.uchar(), boxbuff.uchar(), C.ulonglong(boxbuff.size), nouncebuff.uchar(), pkbuff.uchar(), skbuff.uchar())
|
||||
if res != 0 {
|
||||
return nil, errors.New("crypto_box_open_easy failed")
|
||||
}
|
||||
// return result
|
||||
return resultbuff.Bytes(), nil
|
||||
}
|
||||
|
||||
// generate a new nounce
|
||||
func NewBoxNounce() []byte {
|
||||
return RandBytes(NounceLen())
|
||||
}
|
||||
|
||||
// length of a nounce
|
||||
func NounceLen() int {
|
||||
return int(C.crypto_box_macbytes())
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package nacl
|
||||
|
||||
// #cgo freebsd CFLAGS: -I/usr/local/include
|
||||
// #cgo freebsd LDFLAGS: -L/usr/local/lib
|
||||
// #cgo LDFLAGS: -lsodium
|
||||
// #include <sodium.h>
|
||||
//
|
||||
// unsigned char * deref_uchar(void * ptr) { return (unsigned char*) ptr; }
|
||||
//
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// wrapper arround malloc/free
|
||||
type Buffer struct {
|
||||
ptr unsafe.Pointer
|
||||
length C.int
|
||||
size C.size_t
|
||||
}
|
||||
|
||||
// wrapper arround nacl.malloc
|
||||
func Malloc(size int) *Buffer {
|
||||
if size > 0 {
|
||||
return malloc(C.size_t(size))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// does not check for negatives
|
||||
func malloc(size C.size_t) *Buffer {
|
||||
ptr := C.malloc(size)
|
||||
C.sodium_memzero(ptr, size)
|
||||
buffer := &Buffer{ptr: ptr, size: size, length: C.int(size)}
|
||||
return buffer
|
||||
}
|
||||
|
||||
// create a new buffer copying from a byteslice
|
||||
func NewBuffer(buff []byte) *Buffer {
|
||||
buffer := Malloc(len(buff))
|
||||
if buffer == nil {
|
||||
return nil
|
||||
}
|
||||
if copy(buffer.Data(), buff) != len(buff) {
|
||||
return nil
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func (self *Buffer) uchar() *C.uchar {
|
||||
return C.deref_uchar(self.ptr)
|
||||
}
|
||||
|
||||
func (self *Buffer) Length() int {
|
||||
return int(self.length)
|
||||
}
|
||||
|
||||
// get immutable byte slice
|
||||
func (self *Buffer) Bytes() []byte {
|
||||
buff := make([]byte, self.Length())
|
||||
copy(buff, self.Data())
|
||||
return buff
|
||||
}
|
||||
|
||||
// get underlying byte slice
|
||||
func (self *Buffer) Data() []byte {
|
||||
hdr := reflect.SliceHeader{
|
||||
Data: uintptr(self.ptr),
|
||||
Len: self.Length(),
|
||||
Cap: self.Length(),
|
||||
}
|
||||
return *(*[]byte)(unsafe.Pointer(&hdr))
|
||||
}
|
||||
|
||||
func (self *Buffer) String() string {
|
||||
return hex.EncodeToString(self.Data())
|
||||
}
|
||||
|
||||
// zero out memory and then free
|
||||
func (self *Buffer) Free() {
|
||||
C.sodium_memzero(self.ptr, self.size)
|
||||
C.free(self.ptr)
|
||||
}
|
||||
178
contrib/backends/nntpchand/src/nntpchan/lib/crypto/nacl/key.go
Normal file
178
contrib/backends/nntpchand/src/nntpchan/lib/crypto/nacl/key.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package nacl
|
||||
|
||||
// #cgo freebsd CFLAGS: -I/usr/local/include
|
||||
// #cgo freebsd LDFLAGS: -L/usr/local/lib
|
||||
// #cgo LDFLAGS: -lsodium
|
||||
// #include <sodium.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
pk *Buffer
|
||||
sk *Buffer
|
||||
}
|
||||
|
||||
// free this keypair from memory
|
||||
func (self *KeyPair) Free() {
|
||||
self.pk.Free()
|
||||
self.sk.Free()
|
||||
}
|
||||
|
||||
func (self *KeyPair) Secret() []byte {
|
||||
return self.sk.Bytes()
|
||||
}
|
||||
|
||||
func (self *KeyPair) Public() []byte {
|
||||
return self.pk.Bytes()
|
||||
}
|
||||
|
||||
func (self *KeyPair) Seed() []byte {
|
||||
seed_len := C.crypto_sign_seedbytes()
|
||||
return self.sk.Bytes()[:seed_len]
|
||||
}
|
||||
|
||||
// generate a keypair
|
||||
func GenSignKeypair() *KeyPair {
|
||||
sk_len := C.crypto_sign_secretkeybytes()
|
||||
sk := malloc(sk_len)
|
||||
pk_len := C.crypto_sign_publickeybytes()
|
||||
pk := malloc(pk_len)
|
||||
res := C.crypto_sign_keypair(pk.uchar(), sk.uchar())
|
||||
if res == 0 {
|
||||
return &KeyPair{pk, sk}
|
||||
}
|
||||
pk.Free()
|
||||
sk.Free()
|
||||
return nil
|
||||
}
|
||||
|
||||
// get public key from secret key
|
||||
func GetSignPubkey(sk []byte) ([]byte, error) {
|
||||
sk_len := C.crypto_sign_secretkeybytes()
|
||||
if C.size_t(len(sk)) != sk_len {
|
||||
return nil, errors.New(fmt.Sprintf("nacl.GetSignPubkey() invalid secret key size %d != %d", len(sk), sk_len))
|
||||
}
|
||||
|
||||
pk_len := C.crypto_sign_publickeybytes()
|
||||
pkbuff := malloc(pk_len)
|
||||
defer pkbuff.Free()
|
||||
|
||||
skbuff := NewBuffer(sk)
|
||||
defer skbuff.Free()
|
||||
//XXX: hack
|
||||
res := C.crypto_sign_seed_keypair(pkbuff.uchar(), skbuff.uchar(), skbuff.uchar())
|
||||
|
||||
if res != 0 {
|
||||
return nil, errors.New(fmt.Sprintf("nacl.GetSignPubkey() failed to get public key from secret key: %d", res))
|
||||
}
|
||||
|
||||
return pkbuff.Bytes(), nil
|
||||
}
|
||||
|
||||
// make keypair from seed
|
||||
func LoadSignKey(seed []byte) *KeyPair {
|
||||
seed_len := C.crypto_sign_seedbytes()
|
||||
if C.size_t(len(seed)) != seed_len {
|
||||
return nil
|
||||
}
|
||||
seedbuff := NewBuffer(seed)
|
||||
defer seedbuff.Free()
|
||||
pk_len := C.crypto_sign_publickeybytes()
|
||||
sk_len := C.crypto_sign_secretkeybytes()
|
||||
pkbuff := malloc(pk_len)
|
||||
skbuff := malloc(sk_len)
|
||||
res := C.crypto_sign_seed_keypair(pkbuff.uchar(), skbuff.uchar(), seedbuff.uchar())
|
||||
if res != 0 {
|
||||
pkbuff.Free()
|
||||
skbuff.Free()
|
||||
return nil
|
||||
}
|
||||
return &KeyPair{pkbuff, skbuff}
|
||||
}
|
||||
|
||||
func GenBoxKeypair() *KeyPair {
|
||||
sk_len := C.crypto_box_secretkeybytes()
|
||||
sk := malloc(sk_len)
|
||||
pk_len := C.crypto_box_publickeybytes()
|
||||
pk := malloc(pk_len)
|
||||
res := C.crypto_box_keypair(pk.uchar(), sk.uchar())
|
||||
if res == 0 {
|
||||
return &KeyPair{pk, sk}
|
||||
}
|
||||
pk.Free()
|
||||
sk.Free()
|
||||
return nil
|
||||
}
|
||||
|
||||
// get public key from secret key
|
||||
func GetBoxPubkey(sk []byte) []byte {
|
||||
sk_len := C.crypto_box_seedbytes()
|
||||
if C.size_t(len(sk)) != sk_len {
|
||||
return nil
|
||||
}
|
||||
|
||||
pk_len := C.crypto_box_publickeybytes()
|
||||
pkbuff := malloc(pk_len)
|
||||
defer pkbuff.Free()
|
||||
|
||||
skbuff := NewBuffer(sk)
|
||||
defer skbuff.Free()
|
||||
|
||||
// compute the public key
|
||||
C.crypto_scalarmult_base(pkbuff.uchar(), skbuff.uchar())
|
||||
|
||||
return pkbuff.Bytes()
|
||||
}
|
||||
|
||||
// load keypair from secret key
|
||||
func LoadBoxKey(sk []byte) *KeyPair {
|
||||
pk := GetBoxPubkey(sk)
|
||||
if pk == nil {
|
||||
return nil
|
||||
}
|
||||
pkbuff := NewBuffer(pk)
|
||||
skbuff := NewBuffer(sk)
|
||||
return &KeyPair{pkbuff, skbuff}
|
||||
}
|
||||
|
||||
// make keypair from seed
|
||||
func SeedBoxKey(seed []byte) *KeyPair {
|
||||
seed_len := C.crypto_box_seedbytes()
|
||||
if C.size_t(len(seed)) != seed_len {
|
||||
return nil
|
||||
}
|
||||
seedbuff := NewBuffer(seed)
|
||||
defer seedbuff.Free()
|
||||
pk_len := C.crypto_box_publickeybytes()
|
||||
sk_len := C.crypto_box_secretkeybytes()
|
||||
pkbuff := malloc(pk_len)
|
||||
skbuff := malloc(sk_len)
|
||||
res := C.crypto_box_seed_keypair(pkbuff.uchar(), skbuff.uchar(), seedbuff.uchar())
|
||||
if res != 0 {
|
||||
pkbuff.Free()
|
||||
skbuff.Free()
|
||||
return nil
|
||||
}
|
||||
return &KeyPair{pkbuff, skbuff}
|
||||
}
|
||||
|
||||
func (self *KeyPair) String() string {
|
||||
return fmt.Sprintf("pk=%s sk=%s", hex.EncodeToString(self.pk.Data()), hex.EncodeToString(self.sk.Data()))
|
||||
}
|
||||
|
||||
func CryptoSignPublicLen() int {
|
||||
return int(C.crypto_sign_publickeybytes())
|
||||
}
|
||||
|
||||
func CryptoSignSecretLen() int {
|
||||
return int(C.crypto_sign_secretkeybytes())
|
||||
}
|
||||
|
||||
func CryptoSignSeedLen() int {
|
||||
return int(C.crypto_sign_seedbytes())
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package nacl
|
||||
|
||||
// #cgo freebsd CFLAGS: -I/usr/local/include
|
||||
// #cgo freebsd LDFLAGS: -L/usr/local/lib
|
||||
// #cgo LDFLAGS: -lsodium
|
||||
// #include <sodium.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// return how many bytes overhead does CryptoBox have
|
||||
func CryptoBoxOverhead() int {
|
||||
return int(C.crypto_box_macbytes())
|
||||
}
|
||||
|
||||
// size of crypto_box public keys
|
||||
func CryptoBoxPubKeySize() int {
|
||||
return int(C.crypto_box_publickeybytes())
|
||||
}
|
||||
|
||||
// size of crypto_box private keys
|
||||
func CryptoBoxPrivKeySize() int {
|
||||
return int(C.crypto_box_secretkeybytes())
|
||||
}
|
||||
|
||||
// size of crypto_sign public keys
|
||||
func CryptoSignPubKeySize() int {
|
||||
return int(C.crypto_sign_publickeybytes())
|
||||
}
|
||||
|
||||
// size of crypto_sign private keys
|
||||
func CryptoSignPrivKeySize() int {
|
||||
return int(C.crypto_sign_secretkeybytes())
|
||||
}
|
||||
|
||||
// initialize sodium
|
||||
func init() {
|
||||
status := C.sodium_init()
|
||||
if status == -1 {
|
||||
log.Fatalf("failed to initialize libsodium status=%d", status)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package nacl
|
||||
|
||||
// #cgo freebsd CFLAGS: -I/usr/local/include
|
||||
// #cgo freebsd LDFLAGS: -L/usr/local/lib
|
||||
// #cgo LDFLAGS: -lsodium
|
||||
// #include <sodium.h>
|
||||
import "C"
|
||||
|
||||
func randbytes(size C.size_t) *Buffer {
|
||||
|
||||
buff := malloc(size)
|
||||
C.randombytes_buf(buff.ptr, size)
|
||||
return buff
|
||||
|
||||
}
|
||||
|
||||
func RandBytes(size int) []byte {
|
||||
if size > 0 {
|
||||
buff := randbytes(C.size_t(size))
|
||||
defer buff.Free()
|
||||
return buff.Bytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package nacl
|
||||
|
||||
// #cgo freebsd CFLAGS: -I/usr/local/include
|
||||
// #cgo freebsd LDFLAGS: -L/usr/local/lib
|
||||
// #cgo LDFLAGS: -lsodium
|
||||
// #include <sodium.h>
|
||||
import "C"
|
||||
|
||||
// sign data detached with secret key sk
|
||||
func CryptoSignDetached(msg, sk []byte) []byte {
|
||||
msgbuff := NewBuffer(msg)
|
||||
defer msgbuff.Free()
|
||||
skbuff := NewBuffer(sk)
|
||||
defer skbuff.Free()
|
||||
if skbuff.size != C.crypto_sign_bytes() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// allocate the signature buffer
|
||||
sig := malloc(C.crypto_sign_bytes())
|
||||
defer sig.Free()
|
||||
// compute signature
|
||||
siglen := C.ulonglong(0)
|
||||
res := C.crypto_sign_detached(sig.uchar(), &siglen, msgbuff.uchar(), C.ulonglong(msgbuff.size), skbuff.uchar())
|
||||
if res == 0 && siglen == C.ulonglong(C.crypto_sign_bytes()) {
|
||||
// return copy of signature buffer
|
||||
return sig.Bytes()
|
||||
}
|
||||
// failure to sign
|
||||
return nil
|
||||
}
|
||||
|
||||
// sign data with secret key sk
|
||||
// return detached sig
|
||||
// this uses crypto_sign instead pf crypto_sign_detached
|
||||
func CryptoSignFucky(msg, sk []byte) []byte {
|
||||
msgbuff := NewBuffer(msg)
|
||||
defer msgbuff.Free()
|
||||
skbuff := NewBuffer(sk)
|
||||
defer skbuff.Free()
|
||||
if skbuff.size != C.crypto_sign_bytes() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// allocate the signed message buffer
|
||||
sig := malloc(C.crypto_sign_bytes() + msgbuff.size)
|
||||
defer sig.Free()
|
||||
// compute signature
|
||||
siglen := C.ulonglong(0)
|
||||
res := C.crypto_sign(sig.uchar(), &siglen, msgbuff.uchar(), C.ulonglong(msgbuff.size), skbuff.uchar())
|
||||
if res == 0 {
|
||||
// return copy of signature inside the signed message
|
||||
offset := int(C.crypto_sign_bytes())
|
||||
return sig.Bytes()[:offset]
|
||||
}
|
||||
// failure to sign
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package nacl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TOY encrypted authenticated stream protocol like tls
|
||||
|
||||
var BadHandshake = errors.New("Bad handshake")
|
||||
var ShortWrite = errors.New("short write")
|
||||
var ShortRead = errors.New("short read")
|
||||
var Closed = errors.New("socket closed")
|
||||
|
||||
// write boxes at 512 bytes at a time
|
||||
const DefaultMTU = 512
|
||||
|
||||
// wrapper arround crypto_box
|
||||
// provides an authenticated encrypted stream
|
||||
// this is a TOY
|
||||
type CryptoStream struct {
|
||||
// underlying stream to write on
|
||||
stream io.ReadWriteCloser
|
||||
// secret key seed
|
||||
key *KeyPair
|
||||
// public key of who we expect on the other end
|
||||
remote_pk []byte
|
||||
tx_nonce []byte
|
||||
rx_nonce []byte
|
||||
// box size
|
||||
mtu int
|
||||
}
|
||||
|
||||
func (cs *CryptoStream) Close() (err error) {
|
||||
if cs.key != nil {
|
||||
cs.key.Free()
|
||||
cs.key = nil
|
||||
}
|
||||
return cs.stream.Close()
|
||||
}
|
||||
|
||||
// implements io.Writer
|
||||
func (cs *CryptoStream) Write(data []byte) (n int, err error) {
|
||||
// let's split it up
|
||||
for n < len(data) && err == nil {
|
||||
if n+cs.mtu < len(data) {
|
||||
err = cs.writeSegment(data[n : n+cs.mtu])
|
||||
n += cs.mtu
|
||||
} else {
|
||||
err = cs.writeSegment(data[n:])
|
||||
if err == nil {
|
||||
n = len(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cs *CryptoStream) public() (p []byte) {
|
||||
p = cs.key.Public()
|
||||
return
|
||||
}
|
||||
|
||||
func (cs *CryptoStream) secret() (s []byte) {
|
||||
s = cs.key.Secret()
|
||||
return
|
||||
}
|
||||
|
||||
// read 1 segment
|
||||
func (cs *CryptoStream) readSegment() (s []byte, err error) {
|
||||
var stream_read int
|
||||
var seg []byte
|
||||
nl := NounceLen()
|
||||
msg := make([]byte, cs.mtu+nl)
|
||||
stream_read, err = cs.stream.Read(msg)
|
||||
seg, err = CryptoBoxOpen(msg[:stream_read], cs.rx_nonce, cs.secret(), cs.remote_pk)
|
||||
if err == nil {
|
||||
copy(cs.rx_nonce, seg[:nl])
|
||||
s = seg[nl:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// write 1 segment encrypted
|
||||
// update nounce
|
||||
func (cs *CryptoStream) writeSegment(data []byte) (err error) {
|
||||
var segment []byte
|
||||
nl := NounceLen()
|
||||
msg := make([]byte, len(data)+nl)
|
||||
// generate next nounce
|
||||
nextNounce := NewBoxNounce()
|
||||
copy(msg, nextNounce)
|
||||
copy(msg[nl:], data)
|
||||
// encrypt segment with current nounce
|
||||
segment, err = CryptoBox(data, cs.tx_nonce, cs.remote_pk, cs.secret())
|
||||
var n int
|
||||
n, err = cs.stream.Write(segment)
|
||||
if n != len(segment) {
|
||||
// short write?
|
||||
err = ShortWrite
|
||||
return
|
||||
}
|
||||
// update nounce
|
||||
copy(cs.tx_nonce, nextNounce)
|
||||
return
|
||||
}
|
||||
|
||||
// implements io.Reader
|
||||
func (cs *CryptoStream) Read(data []byte) (n int, err error) {
|
||||
var seg []byte
|
||||
seg, err = cs.readSegment()
|
||||
if err == nil {
|
||||
if len(seg) <= len(data) {
|
||||
copy(data, seg)
|
||||
n = len(seg)
|
||||
} else {
|
||||
// too big?
|
||||
err = ShortRead
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// version 0 protocol magic
|
||||
var protocol_magic = []byte("BENIS|00")
|
||||
|
||||
// verify that a handshake is signed right and is in the correct format etc
|
||||
func verifyHandshake(hs, pk []byte) (valid bool) {
|
||||
ml := len(protocol_magic)
|
||||
// valid handshake?
|
||||
if bytes.Equal(hs[0:ml], protocol_magic) {
|
||||
// check pk
|
||||
pl := CryptoSignPublicLen()
|
||||
nl := NounceLen()
|
||||
if bytes.Equal(pk, hs[ml:ml+pl]) {
|
||||
// check signature
|
||||
msg := hs[0 : ml+pl+nl]
|
||||
sig := hs[ml+pl+nl:]
|
||||
valid = CryptoVerifyFucky(msg, sig, pk)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// get claimed public key from handshake
|
||||
func getPubkey(hs []byte) (pk []byte) {
|
||||
ml := len(protocol_magic)
|
||||
pl := CryptoSignPublicLen()
|
||||
pk = hs[ml : ml+pl]
|
||||
return
|
||||
}
|
||||
|
||||
func (cs *CryptoStream) genHandshake() (d []byte) {
|
||||
// protocol magic string version 00
|
||||
// Benis Encrypted Network Information Stream
|
||||
// :-DDDDD meme crypto
|
||||
d = append(d, protocol_magic...)
|
||||
// our public key
|
||||
d = append(d, cs.public()...)
|
||||
// nounce
|
||||
cs.tx_nonce = NewBoxNounce()
|
||||
d = append(d, cs.tx_nonce...)
|
||||
// sign protocol magic string, nounce and pubkey
|
||||
sig := CryptoSignFucky(d, cs.secret())
|
||||
// if sig is nil we'll just die
|
||||
d = append(d, sig...)
|
||||
return
|
||||
}
|
||||
|
||||
// extract nounce from handshake
|
||||
func getNounce(hs []byte) (n []byte) {
|
||||
ml := len(protocol_magic)
|
||||
pl := CryptoSignPublicLen()
|
||||
nl := NounceLen()
|
||||
n = hs[ml+pl : ml+pl+nl]
|
||||
return
|
||||
}
|
||||
|
||||
// initiate protocol handshake
|
||||
func (cs *CryptoStream) Handshake() (err error) {
|
||||
// send them our info
|
||||
hs := cs.genHandshake()
|
||||
var n int
|
||||
n, err = cs.stream.Write(hs)
|
||||
if n != len(hs) {
|
||||
err = ShortWrite
|
||||
return
|
||||
}
|
||||
// read thier info
|
||||
buff := make([]byte, len(hs))
|
||||
_, err = io.ReadFull(cs.stream, buff)
|
||||
|
||||
if cs.remote_pk == nil {
|
||||
// inbound
|
||||
pk := getPubkey(buff)
|
||||
cs.remote_pk = make([]byte, len(pk))
|
||||
copy(cs.remote_pk, pk)
|
||||
}
|
||||
|
||||
if !verifyHandshake(buff, cs.remote_pk) {
|
||||
// verification failed
|
||||
err = BadHandshake
|
||||
return
|
||||
}
|
||||
cs.rx_nonce = make([]byte, NounceLen())
|
||||
copy(cs.rx_nonce, getNounce(buff))
|
||||
return
|
||||
}
|
||||
|
||||
// create a client
|
||||
func Client(stream io.ReadWriteCloser, local_sk, remote_pk []byte) (c *CryptoStream) {
|
||||
c = &CryptoStream{
|
||||
stream: stream,
|
||||
mtu: DefaultMTU,
|
||||
}
|
||||
c.remote_pk = make([]byte, len(remote_pk))
|
||||
copy(c.remote_pk, remote_pk)
|
||||
c.key = LoadSignKey(local_sk)
|
||||
if c.key == nil {
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type CryptoConn struct {
|
||||
stream *CryptoStream
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (cc *CryptoConn) Close() (err error) {
|
||||
err = cc.stream.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func (cc *CryptoConn) Write(d []byte) (n int, err error) {
|
||||
return cc.stream.Write(d)
|
||||
}
|
||||
|
||||
func (cc *CryptoConn) Read(d []byte) (n int, err error) {
|
||||
return cc.stream.Read(d)
|
||||
}
|
||||
|
||||
func (cc *CryptoConn) LocalAddr() net.Addr {
|
||||
return cc.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (cc *CryptoConn) RemoteAddr() net.Addr {
|
||||
return cc.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (cc *CryptoConn) SetDeadline(t time.Time) (err error) {
|
||||
return cc.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (cc *CryptoConn) SetReadDeadline(t time.Time) (err error) {
|
||||
return cc.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (cc *CryptoConn) SetWriteDeadline(t time.Time) (err error) {
|
||||
return cc.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
type CryptoListener struct {
|
||||
l net.Listener
|
||||
handshake chan net.Conn
|
||||
accepted chan *CryptoConn
|
||||
trust func(pk []byte) bool
|
||||
key *KeyPair
|
||||
}
|
||||
|
||||
func (cl *CryptoListener) Close() (err error) {
|
||||
err = cl.l.Close()
|
||||
close(cl.accepted)
|
||||
close(cl.handshake)
|
||||
cl.key.Free()
|
||||
cl.key = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (cl *CryptoListener) acceptInbound() {
|
||||
for {
|
||||
c, err := cl.l.Accept()
|
||||
if err == nil {
|
||||
cl.handshake <- c
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cl *CryptoListener) runChans() {
|
||||
for {
|
||||
select {
|
||||
case c := <-cl.handshake:
|
||||
go func() {
|
||||
s := &CryptoStream{
|
||||
stream: c,
|
||||
mtu: DefaultMTU,
|
||||
key: cl.key,
|
||||
}
|
||||
err := s.Handshake()
|
||||
if err == nil {
|
||||
// we gud handshake was okay
|
||||
if cl.trust(s.remote_pk) {
|
||||
// the key is trusted okay
|
||||
cl.accepted <- &CryptoConn{stream: s, conn: c}
|
||||
} else {
|
||||
// not trusted, close connection
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// accept inbound authenticated and trusted connections
|
||||
func (cl *CryptoListener) Accept() (c net.Conn, err error) {
|
||||
var ok bool
|
||||
c, ok = <-cl.accepted
|
||||
if !ok {
|
||||
err = Closed
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// create a listener
|
||||
func Server(l net.Listener, local_sk []byte, trust func(pk []byte) bool) (s *CryptoListener) {
|
||||
s = &CryptoListener{
|
||||
l: l,
|
||||
trust: trust,
|
||||
handshake: make(chan net.Conn),
|
||||
accepted: make(chan *CryptoConn),
|
||||
}
|
||||
s.key = LoadSignKey(local_sk)
|
||||
go s.runChans()
|
||||
go s.acceptInbound()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package nacl
|
||||
|
||||
// #cgo freebsd CFLAGS: -I/usr/local/include
|
||||
// #cgo freebsd LDFLAGS: -L/usr/local/lib
|
||||
// #cgo LDFLAGS: -lsodium
|
||||
// #include <sodium.h>
|
||||
import "C"
|
||||
|
||||
// verify a fucky detached sig
|
||||
func CryptoVerifyFucky(msg, sig, pk []byte) bool {
|
||||
var smsg []byte
|
||||
smsg = append(smsg, sig...)
|
||||
smsg = append(smsg, msg...)
|
||||
return CryptoVerify(smsg, pk)
|
||||
}
|
||||
|
||||
// verify a signed message
|
||||
func CryptoVerify(smsg, pk []byte) bool {
|
||||
smsg_buff := NewBuffer(smsg)
|
||||
defer smsg_buff.Free()
|
||||
pk_buff := NewBuffer(pk)
|
||||
defer pk_buff.Free()
|
||||
|
||||
if pk_buff.size != C.crypto_sign_publickeybytes() {
|
||||
return false
|
||||
}
|
||||
mlen := C.ulonglong(0)
|
||||
msg := malloc(C.size_t(len(smsg)))
|
||||
defer msg.Free()
|
||||
smlen := C.ulonglong(smsg_buff.size)
|
||||
return C.crypto_sign_open(msg.uchar(), &mlen, smsg_buff.uchar(), smlen, pk_buff.uchar()) != -1
|
||||
}
|
||||
|
||||
// verfiy a detached signature
|
||||
// return true on valid otherwise false
|
||||
func CryptoVerifyDetached(msg, sig, pk []byte) bool {
|
||||
msg_buff := NewBuffer(msg)
|
||||
defer msg_buff.Free()
|
||||
sig_buff := NewBuffer(sig)
|
||||
defer sig_buff.Free()
|
||||
pk_buff := NewBuffer(pk)
|
||||
defer pk_buff.Free()
|
||||
|
||||
if pk_buff.size != C.crypto_sign_publickeybytes() {
|
||||
return false
|
||||
}
|
||||
|
||||
// invalid sig size
|
||||
if sig_buff.size != C.crypto_sign_bytes() {
|
||||
return false
|
||||
}
|
||||
return C.crypto_sign_verify_detached(sig_buff.uchar(), msg_buff.uchar(), C.ulonglong(len(msg)), pk_buff.uchar()) == 0
|
||||
}
|
||||
@@ -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,8 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"nntpchan/lib/crypto/nacl"
|
||||
)
|
||||
|
||||
// generate random bytes
|
||||
var RandBytes = nacl.RandBytes
|
||||
25
contrib/backends/nntpchand/src/nntpchan/lib/crypto/sig.go
Normal file
25
contrib/backends/nntpchand/src/nntpchan/lib/crypto/sig.go
Normal file
@@ -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)
|
||||
}
|
||||
14
contrib/backends/nntpchand/src/nntpchan/lib/crypto/sign.go
Normal file
14
contrib/backends/nntpchand/src/nntpchan/lib/crypto/sign.go
Normal file
@@ -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
|
||||
}
|
||||
14
contrib/backends/nntpchand/src/nntpchan/lib/crypto/verify.go
Normal file
14
contrib/backends/nntpchand/src/nntpchan/lib/crypto/verify.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"nntpchan/lib/config"
|
||||
"nntpchan/lib/model"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//
|
||||
type Database interface {
|
||||
ThreadByMessageID(msgid string) (*model.Thread, error)
|
||||
ThreadByHash(hash string) (*model.Thread, error)
|
||||
BoardPage(newsgroup string, pageno, perpage int) (*model.BoardPage, error)
|
||||
}
|
||||
|
||||
// get new database connector from configuration
|
||||
func NewDBFromConfig(c *config.DatabaseConfig) (db Database, err error) {
|
||||
dbtype := strings.ToLower(c.Type)
|
||||
if dbtype == "postgres" {
|
||||
db, err = createPostgresDatabase(c.Addr, c.Username, c.Password)
|
||||
} else {
|
||||
err = errors.New("no such database driver: " + c.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//
|
||||
// database driver
|
||||
//
|
||||
package database
|
||||
@@ -0,0 +1,28 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"nntpchan/lib/model"
|
||||
)
|
||||
|
||||
type PostgresDB struct {
|
||||
}
|
||||
|
||||
func (db *PostgresDB) ThreadByMessageID(msgid string) (thread *model.Thread, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (db *PostgresDB) ThreadByHash(hash string) (thread *model.Thread, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (db *PostgresDB) BoardPage(newsgroup string, pageno, perpage int) (page *model.BoardPage, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func createPostgresDatabase(addr, user, passwd string) (p *PostgresDB, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
123
contrib/backends/nntpchand/src/nntpchan/lib/frontend/captcha.go
Normal file
123
contrib/backends/nntpchand/src/nntpchan/lib/frontend/captcha.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dchest/captcha"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"net/http"
|
||||
"nntpchan/lib/config"
|
||||
)
|
||||
|
||||
// server of captchas
|
||||
// implements frontend.Middleware
|
||||
type CaptchaServer struct {
|
||||
h int
|
||||
w int
|
||||
store *sessions.CookieStore
|
||||
prefix string
|
||||
sessionName string
|
||||
}
|
||||
|
||||
// create new captcha server using existing session store
|
||||
func NewCaptchaServer(w, h int, prefix string, store *sessions.CookieStore) *CaptchaServer {
|
||||
return &CaptchaServer{
|
||||
h: h,
|
||||
w: w,
|
||||
prefix: prefix,
|
||||
store: store,
|
||||
sessionName: "captcha",
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *CaptchaServer) Reload(c *config.MiddlewareConfig) {
|
||||
|
||||
}
|
||||
|
||||
func (cs *CaptchaServer) SetupRoutes(m *mux.Router) {
|
||||
m.Path("/new").HandlerFunc(cs.NewCaptcha)
|
||||
m.Path("/img/{f}").Handler(captcha.Server(cs.w, cs.h))
|
||||
m.Path("/verify.json").HandlerFunc(cs.VerifyCaptcha)
|
||||
}
|
||||
|
||||
// return true if this session has solved the last captcha given provided solution, otherwise false
|
||||
func (cs *CaptchaServer) CheckSession(w http.ResponseWriter, r *http.Request, solution string) (bool, error) {
|
||||
s, err := cs.store.Get(r, cs.sessionName)
|
||||
if err == nil {
|
||||
id, ok := s.Values["captcha_id"]
|
||||
if ok {
|
||||
return captcha.VerifyString(id.(string), solution), nil
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// verify a captcha
|
||||
func (cs *CaptchaServer) VerifyCaptcha(w http.ResponseWriter, r *http.Request) {
|
||||
dec := json.NewDecoder(r.Body)
|
||||
defer r.Body.Close()
|
||||
// request
|
||||
req := make(map[string]string)
|
||||
// response
|
||||
resp := make(map[string]interface{})
|
||||
resp["solved"] = false
|
||||
// decode request
|
||||
err := dec.Decode(req)
|
||||
if err == nil {
|
||||
// decode okay
|
||||
id, ok := req["id"]
|
||||
if ok {
|
||||
// we have id
|
||||
solution, ok := req["solution"]
|
||||
if ok {
|
||||
// we have solution and id
|
||||
resp["solved"] = captcha.VerifyString(id, solution)
|
||||
} else {
|
||||
// we don't have solution
|
||||
err = errors.New("no captcha solution provided")
|
||||
}
|
||||
} else {
|
||||
// we don't have id
|
||||
err = errors.New("no captcha id provided")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// error happened
|
||||
resp["error"] = err.Error()
|
||||
}
|
||||
// send reply
|
||||
w.Header().Set("Content-Type", "text/json; encoding=UTF-8")
|
||||
enc := json.NewEncoder(w)
|
||||
enc.Encode(resp)
|
||||
}
|
||||
|
||||
// generate a new captcha
|
||||
func (cs *CaptchaServer) NewCaptcha(w http.ResponseWriter, r *http.Request) {
|
||||
// obtain session
|
||||
sess, err := cs.store.Get(r, cs.sessionName)
|
||||
if err != nil {
|
||||
// failed to obtain session
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// new captcha
|
||||
id := captcha.New()
|
||||
// do we want to interpret as json?
|
||||
use_json := r.URL.Query().Get("t") == "json"
|
||||
// image url
|
||||
url := fmt.Sprintf("%simg/%s.png", cs.prefix, id)
|
||||
if use_json {
|
||||
// send json
|
||||
enc := json.NewEncoder(w)
|
||||
enc.Encode(map[string]string{"id": id, "url": url})
|
||||
} else {
|
||||
// set captcha id
|
||||
sess.Values["captcha_id"] = id
|
||||
// save session
|
||||
sess.Save(r, w)
|
||||
// rediect to image
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// nntpchan frontend
|
||||
// allows posting to nntpchan network via various implementations
|
||||
//
|
||||
package frontend
|
||||
@@ -0,0 +1,46 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"nntpchan/lib/config"
|
||||
"nntpchan/lib/database"
|
||||
"nntpchan/lib/model"
|
||||
"nntpchan/lib/nntp"
|
||||
)
|
||||
|
||||
// a frontend that displays nntp posts and allows posting
|
||||
type Frontend interface {
|
||||
|
||||
// run mainloop
|
||||
Serve()
|
||||
|
||||
// do we accept this inbound post?
|
||||
AllowPost(p model.PostReference) bool
|
||||
|
||||
// trigger a manual regen of indexes for a root post
|
||||
Regen(p model.PostReference)
|
||||
|
||||
// implements nntp.EventHooks
|
||||
GotArticle(msgid nntp.MessageID, group nntp.Newsgroup)
|
||||
|
||||
// implements nntp.EventHooks
|
||||
SentArticleVia(msgid nntp.MessageID, feedname string)
|
||||
|
||||
// reload config
|
||||
Reload(c *config.FrontendConfig)
|
||||
}
|
||||
|
||||
// create a new http frontend give frontend config
|
||||
func NewHTTPFrontend(c *config.FrontendConfig, db database.Database) (f Frontend, err error) {
|
||||
|
||||
var mid Middleware
|
||||
if c.Middleware != nil {
|
||||
// middleware configured
|
||||
mid, err = OverchanMiddleware(c.Middleware, db)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// create http frontend only if no previous errors
|
||||
f, err = createHttpFrontend(c, mid, db)
|
||||
}
|
||||
return
|
||||
}
|
||||
136
contrib/backends/nntpchand/src/nntpchan/lib/frontend/http.go
Normal file
136
contrib/backends/nntpchand/src/nntpchan/lib/frontend/http.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"nntpchan/lib/admin"
|
||||
"nntpchan/lib/api"
|
||||
"nntpchan/lib/config"
|
||||
"nntpchan/lib/database"
|
||||
"nntpchan/lib/model"
|
||||
"nntpchan/lib/nntp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// http frontend server
|
||||
// provides glue layer between nntp and middleware
|
||||
type httpFrontend struct {
|
||||
// bind address
|
||||
addr string
|
||||
// http mux
|
||||
httpmux *mux.Router
|
||||
// admin panel
|
||||
adminPanel *admin.Server
|
||||
// static files path
|
||||
staticDir string
|
||||
// http middleware
|
||||
middleware Middleware
|
||||
// api server
|
||||
apiserve *api.Server
|
||||
// database driver
|
||||
db database.Database
|
||||
}
|
||||
|
||||
// reload http frontend
|
||||
// reloads middleware
|
||||
func (f *httpFrontend) Reload(c *config.FrontendConfig) {
|
||||
if f.middleware == nil {
|
||||
if c.Middleware != nil {
|
||||
var err error
|
||||
// no middleware set, create middleware
|
||||
f.middleware, err = OverchanMiddleware(c.Middleware, f.db)
|
||||
if err != nil {
|
||||
log.Errorf("overchan middleware reload failed: %s", err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
// middleware exists
|
||||
// do middleware reload
|
||||
f.middleware.Reload(c.Middleware)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// serve http requests from net.Listener
|
||||
func (f *httpFrontend) Serve() {
|
||||
// serve http
|
||||
for {
|
||||
err := http.ListenAndServe(f.addr, f.httpmux)
|
||||
if err != nil {
|
||||
log.Errorf("failed to listen and serve with frontend: %s", err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// serve robots.txt page
|
||||
func (f *httpFrontend) serveRobots(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "User-Agent: *\nDisallow: /\n")
|
||||
}
|
||||
|
||||
func (f *httpFrontend) AllowPost(p model.PostReference) bool {
|
||||
// TODO: implement
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *httpFrontend) Regen(p model.PostReference) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
func (f *httpFrontend) GotArticle(msgid nntp.MessageID, group nntp.Newsgroup) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
func (f *httpFrontend) SentArticleVia(msgid nntp.MessageID, feedname string) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
func createHttpFrontend(c *config.FrontendConfig, mid Middleware, db database.Database) (f *httpFrontend, err error) {
|
||||
f = new(httpFrontend)
|
||||
// set db
|
||||
// db.Ensure() called elsewhere
|
||||
f.db = db
|
||||
|
||||
// set bind address
|
||||
f.addr = c.BindAddr
|
||||
|
||||
// set up mux
|
||||
f.httpmux = mux.NewRouter()
|
||||
|
||||
// set up admin panel
|
||||
f.adminPanel = admin.NewServer()
|
||||
|
||||
// set static files dir
|
||||
f.staticDir = c.Static
|
||||
|
||||
// set middleware
|
||||
f.middleware = mid
|
||||
|
||||
// set up routes
|
||||
|
||||
if f.adminPanel != nil {
|
||||
// route up admin panel
|
||||
f.httpmux.PathPrefix("/admin/").Handler(f.adminPanel)
|
||||
}
|
||||
|
||||
if f.middleware != nil {
|
||||
// route up middleware
|
||||
f.middleware.SetupRoutes(f.httpmux)
|
||||
}
|
||||
|
||||
if f.apiserve != nil {
|
||||
// route up api
|
||||
f.apiserve.SetupRoutes(f.httpmux.PathPrefix("/api/").Subrouter())
|
||||
}
|
||||
|
||||
// route up robots.txt
|
||||
f.httpmux.Path("/robots.txt").HandlerFunc(f.serveRobots)
|
||||
|
||||
// route up static files
|
||||
f.httpmux.PathPrefix("/static/").Handler(http.FileServer(http.Dir(f.staticDir)))
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"nntpchan/lib/config"
|
||||
)
|
||||
|
||||
// http middleware
|
||||
type Middleware interface {
|
||||
// set up routes
|
||||
SetupRoutes(m *mux.Router)
|
||||
// reload with new configuration
|
||||
Reload(c *config.MiddlewareConfig)
|
||||
}
|
||||
115
contrib/backends/nntpchand/src/nntpchan/lib/frontend/overchan.go
Normal file
115
contrib/backends/nntpchand/src/nntpchan/lib/frontend/overchan.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"nntpchan/lib/config"
|
||||
"nntpchan/lib/database"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// standard overchan imageboard middleware
|
||||
type overchanMiddleware struct {
|
||||
templ *template.Template
|
||||
captcha *CaptchaServer
|
||||
store *sessions.CookieStore
|
||||
db database.Database
|
||||
}
|
||||
|
||||
func (m *overchanMiddleware) SetupRoutes(mux *mux.Router) {
|
||||
// setup front page handler
|
||||
mux.Path("/").HandlerFunc(m.ServeIndex)
|
||||
// setup thread handler
|
||||
mux.Path("/t/{id}/").HandlerFunc(m.ServeThread)
|
||||
// setup board page handler
|
||||
mux.Path("/b/{name}/").HandlerFunc(m.ServeBoardPage)
|
||||
// setup posting endpoint
|
||||
mux.Path("/post")
|
||||
// create captcha
|
||||
captchaPrefix := "/captcha/"
|
||||
m.captcha = NewCaptchaServer(200, 400, captchaPrefix, m.store)
|
||||
// setup captcha endpoint
|
||||
m.captcha.SetupRoutes(mux.PathPrefix(captchaPrefix).Subrouter())
|
||||
}
|
||||
|
||||
// reload middleware
|
||||
func (m *overchanMiddleware) Reload(c *config.MiddlewareConfig) {
|
||||
// reload templates
|
||||
templ, err := template.ParseGlob(filepath.Join(c.Templates, "*.tmpl"))
|
||||
if err == nil {
|
||||
log.Infof("middleware reloaded templates")
|
||||
m.templ = templ
|
||||
} else {
|
||||
log.Errorf("middleware reload failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *overchanMiddleware) ServeBoardPage(w http.ResponseWriter, r *http.Request) {
|
||||
param := mux.Vars(r)
|
||||
board := param["name"]
|
||||
page := r.URL.Query().Get("q")
|
||||
pageno, err := strconv.Atoi(page)
|
||||
if err == nil {
|
||||
var obj interface{}
|
||||
obj, err = m.db.BoardPage(board, pageno, 10)
|
||||
if err == nil {
|
||||
m.serveTemplate(w, r, "board.html.tmpl", obj)
|
||||
} else {
|
||||
m.serveTemplate(w, r, "error.html.tmpl", err)
|
||||
}
|
||||
} else {
|
||||
// 404
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// serve cached thread
|
||||
func (m *overchanMiddleware) ServeThread(w http.ResponseWriter, r *http.Request) {
|
||||
param := mux.Vars(r)
|
||||
obj, err := m.db.ThreadByHash(param["id"])
|
||||
if err == nil {
|
||||
m.serveTemplate(w, r, "thread.html.tmpl", obj)
|
||||
} else {
|
||||
m.serveTemplate(w, r, "error.html.tmpl", err)
|
||||
}
|
||||
}
|
||||
|
||||
// serve index page
|
||||
func (m *overchanMiddleware) ServeIndex(w http.ResponseWriter, r *http.Request) {
|
||||
m.serveTemplate(w, r, "index.html.tmpl", nil)
|
||||
}
|
||||
|
||||
// serve a template
|
||||
func (m *overchanMiddleware) serveTemplate(w http.ResponseWriter, r *http.Request, tname string, obj interface{}) {
|
||||
t := m.templ.Lookup(tname)
|
||||
if t == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"template": tname,
|
||||
}).Warning("template not found")
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
err := t.Execute(w, obj)
|
||||
if err != nil {
|
||||
// error getting model
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"template": tname,
|
||||
}).Warning("failed to render template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create standard overchan middleware
|
||||
func OverchanMiddleware(c *config.MiddlewareConfig, db database.Database) (m Middleware, err error) {
|
||||
om := new(overchanMiddleware)
|
||||
om.templ, err = template.ParseGlob(filepath.Join(c.Templates, "*.tmpl"))
|
||||
om.db = db
|
||||
if err == nil {
|
||||
m = om
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package frontend
|
||||
@@ -0,0 +1 @@
|
||||
package frontend
|
||||
15
contrib/backends/nntpchand/src/nntpchan/lib/model/article.go
Normal file
15
contrib/backends/nntpchand/src/nntpchan/lib/model/article.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
type Article struct {
|
||||
Subject string
|
||||
Name string
|
||||
Header map[string][]string
|
||||
Text string
|
||||
Attachments []Attachment
|
||||
MessageID string
|
||||
Newsgroup string
|
||||
Reference string
|
||||
Path string
|
||||
Posted int64
|
||||
Addr string
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
type Attachment struct {
|
||||
Path string
|
||||
Name string
|
||||
Mime string
|
||||
Hash string
|
||||
// only filled for api
|
||||
Body string
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package model
|
||||
|
||||
type Board struct {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
type BoardPage struct {
|
||||
Name string
|
||||
Page int
|
||||
Pages int
|
||||
Threads []Thread
|
||||
}
|
||||
2
contrib/backends/nntpchand/src/nntpchan/lib/model/doc.go
Normal file
2
contrib/backends/nntpchand/src/nntpchan/lib/model/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// MVC models
|
||||
package model
|
||||
29
contrib/backends/nntpchand/src/nntpchan/lib/model/misc.go
Normal file
29
contrib/backends/nntpchand/src/nntpchan/lib/model/misc.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ArticleHeader map[string][]string
|
||||
|
||||
// a ( MessageID , newsgroup ) tuple
|
||||
type ArticleEntry [2]string
|
||||
|
||||
func (self ArticleEntry) Newsgroup() string {
|
||||
return self[1]
|
||||
}
|
||||
|
||||
func (self ArticleEntry) MessageID() string {
|
||||
return self[0]
|
||||
}
|
||||
|
||||
// a ( time point, post count ) tuple
|
||||
type PostEntry [2]int64
|
||||
|
||||
func (self PostEntry) Time() time.Time {
|
||||
return time.Unix(self[0], 0)
|
||||
}
|
||||
|
||||
func (self PostEntry) Count() int64 {
|
||||
return self[1]
|
||||
}
|
||||
32
contrib/backends/nntpchand/src/nntpchan/lib/model/post.go
Normal file
32
contrib/backends/nntpchand/src/nntpchan/lib/model/post.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Tripcode string
|
||||
|
||||
type Post struct {
|
||||
MessageID string
|
||||
Newsgroup string
|
||||
Attachments []Attachment
|
||||
Subject string
|
||||
Posted time.Time
|
||||
PostedAt uint64
|
||||
Name string
|
||||
Tripcode Tripcode
|
||||
}
|
||||
|
||||
// ( message-id, references, newsgroup )
|
||||
type PostReference [3]string
|
||||
|
||||
func (r PostReference) MessageID() string {
|
||||
return r[0]
|
||||
}
|
||||
|
||||
func (r PostReference) References() string {
|
||||
return r[1]
|
||||
}
|
||||
func (r PostReference) Newsgroup() string {
|
||||
return r[2]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Thread struct {
|
||||
Root *Post
|
||||
Replies []*Post
|
||||
}
|
||||
37
contrib/backends/nntpchand/src/nntpchan/lib/network/dial.go
Normal file
37
contrib/backends/nntpchand/src/nntpchan/lib/network/dial.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"nntpchan/lib/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// operation timed out
|
||||
var ErrTimeout = errors.New("timeout")
|
||||
|
||||
// the operation was reset abruptly
|
||||
var ErrReset = errors.New("reset")
|
||||
|
||||
// the operation was actively refused
|
||||
var ErrRefused = errors.New("refused")
|
||||
|
||||
// generic dialer
|
||||
// dials out to a remote address
|
||||
// returns a net.Conn and nil on success
|
||||
// returns nil and error if an error happens while dialing
|
||||
type Dialer interface {
|
||||
Dial(remote string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// create a new dialer from configuration
|
||||
func NewDialer(conf *config.ProxyConfig) (d Dialer) {
|
||||
d = StdDialer
|
||||
if conf != nil {
|
||||
proxyType := strings.ToLower(conf.Type)
|
||||
if proxyType == "socks" || proxyType == "socks4a" {
|
||||
d = SocksDialer(conf.Addr)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user