1
0
forked from iarv/nntpchan

15 Commits

Author SHA1 Message Date
Jeff Becker
475e0b8ada id -> class 2016-06-10 07:39:40 -04:00
Jeff Becker
3fbcc30484 correct bad stylesheet link 2016-06-10 07:37:35 -04:00
Jeff Becker
bfb56d9c67 bootstrap ukko page move navbar 2016-06-10 07:35:10 -04:00
Jeff Becker
4932222c4b update bootstrap theme 2016-06-10 07:33:30 -04:00
Jeff Becker
a8264c8f62 update bootstrap theme 2016-06-10 07:31:33 -04:00
Jeff Becker
5251cc860d update bootstrap theme 2016-06-10 07:28:16 -04:00
Jeff Becker
1203b9f712 add initial bootstrap theme parts 2016-06-10 07:16:07 -04:00
Jeff Becker
abbf5fc6aa remove bootstrap option 2016-06-10 07:15:26 -04:00
Jeff Becker
c8d897cc25 move bg.png 2016-06-10 07:12:35 -04:00
Jeff Becker
60bb9d60aa update post.mustache references 2016-06-10 07:09:24 -04:00
Jeff Becker
e221d660fd update gitignore 2016-06-10 07:05:34 -04:00
Jeff Becker
18efd03138 update build-js.sh 2016-06-10 07:04:16 -04:00
Jeff Becker
2b7adc8e99 update references in templates 2016-06-10 07:03:19 -04:00
Jeff Becker
8a961e21fa add bootstrap theme 2016-06-10 07:00:36 -04:00
Jeff Becker
4038d0e394 restructure and add bootstrap 2016-06-10 06:45:23 -04:00
1474 changed files with 11503 additions and 534369 deletions

7
.gitignore vendored
View File

@@ -19,22 +19,23 @@ 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/miner-js.js
contrib/static/js/miner-js.js
#docs trash
doc/.trash

34
.gxignore Normal file
View File

@@ -0,0 +1,34 @@
#
# .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

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-2017 Jeff Becker
Copyright (c) 2015 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

View File

@@ -1,40 +0,0 @@
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)

View File

@@ -1,53 +1,39 @@
[NNTPChan](https://nntpchan.info)
=================================
NNTPChan
========
![le ebin logo](nntpchan.png "ebin logo")
**NNTPChan** (previously known as overchan) is a decentralized imageboard that uses the [NNTP protocol](https://en.wikipedia.org/wiki/Network_News_Transfer_Protocol) (network-news protocol) to synchronize content between many different servers. It utilizes cryptographically signed posts to perform optional/opt-in decentralized moderation.
**NNTPChan** (previously known as overchan) is a decentralized imageboard that uses the [NNTP protocol](https://en.wikipedia.org/wiki/Network_News_Transfer_Protocol) (network-news transfer protocol) to synchronize content between many different servers. It utilizes cryptographically signed posts to perform optional/opt-in decentralized moderation.
This repository contains resources used by the core daemon which is located on [GitHub](https://github.com/majestrate/srndv2) (for now) along with general documentation, [here](doc/)
## Getting started
##Getting started
[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.
[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.
## 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.
## Clients
##Active NNTPChan nodes
NNTP (confirmed working):
Below is a list of known NNTPChan nodes:
* Thunderbird
1. [2hu-ch.org](https://2hu-ch.org)
2. [nsfl.tk](https://nsfl.tk)
3. [i2p.rocks](https://i2p.rocks/ib/)
Web:
* [Yukko](https://github.com/faissaloo/Yukko): ncurses based nntpchan web ui reader
## Support
##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
## History
##Donations
* 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)
![network topology of 4 years](topology.png "changolia")
## Donations
Like this project? Why not help by funding it? This address pays for the server that runs `2hu-ch.org`
Like this project? Why not help by funding it?
Bitcoin: [15yuMzuueV8y5vPQQ39ZqQVz5Ey98DNrjE](bitcoin://15yuMzuueV8y5vPQQ39ZqQVz5Ey98DNrjE)
##Acknowledgements
## Acknowledgements
* [Deavmi](https://deavmi.carteronline.net/) - Making the documentation beautiful.
* [Deavmi](deavmi.carteronline.net/~deavmi) - Making the documentation beautiful.

View File

@@ -4,4 +4,3 @@
* more alternative templates
* javascript free mod panel
* liveui
* easier peering

71
build-js.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/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 Executable file
View File

@@ -0,0 +1,92 @@
#!/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"

View File

@@ -1,5 +0,0 @@
*.o
nntpd
nntpchan-tool
test
.gdb_history

View File

@@ -1,40 +0,0 @@
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

View File

@@ -1,2 +0,0 @@
#!/bin/sh
exit 0

View File

@@ -1,6 +0,0 @@
[nntp]
bind = [::]:1199
authdb=auth.txt
[storage]
path = ./storage/

View File

@@ -1,103 +0,0 @@
#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;
}
}

View File

@@ -1,234 +0,0 @@
#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;
}
}

View File

@@ -1,17 +0,0 @@
#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

View File

@@ -1,21 +0,0 @@
#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;
}
}

View File

@@ -1,19 +0,0 @@
#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

View File

@@ -1,9 +0,0 @@
#include "crypto.hpp"
namespace nntpchan
{
void SHA512(const uint8_t * d, const std::size_t l, SHA512Digest & h)
{
crypto_hash(h.data(), d, l);
}
}

View File

@@ -1,15 +0,0 @@
#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

View File

@@ -1,27 +0,0 @@
#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);
}
}

View File

@@ -1,26 +0,0 @@
#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

View File

@@ -1,57 +0,0 @@
#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;
}
}

View File

@@ -1,30 +0,0 @@
#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

View File

@@ -1,25 +0,0 @@
#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

View File

@@ -1,186 +0,0 @@
/**
* 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

View File

@@ -1,44 +0,0 @@
#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);
}
}

View File

@@ -1,35 +0,0 @@
#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

View File

@@ -1,29 +0,0 @@
#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;
}
}

View File

@@ -1,36 +0,0 @@
#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

View File

@@ -1,44 +0,0 @@
#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;
}
}

View File

@@ -1,23 +0,0 @@
#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

View File

@@ -1,98 +0,0 @@
#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;
}
}

View File

@@ -1,57 +0,0 @@
#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

View File

@@ -1,117 +0,0 @@
#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;
}
}

View File

@@ -1,53 +0,0 @@
#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

View File

@@ -1,149 +0,0 @@
#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;
});
}
}

View File

@@ -1,80 +0,0 @@
#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

View File

@@ -1,46 +0,0 @@
#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 '/';
}
}

View File

@@ -1,36 +0,0 @@
#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

View File

@@ -1,13 +0,0 @@
#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;
}

View File

@@ -1,91 +0,0 @@
#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;
}

View File

@@ -1,11 +0,0 @@
REPO=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
all: clean build
build: nntpchand
nntpchand:
GOPATH=$(REPO) go build -v
clean:
GOPATH=$(REPO) go clean -v

View File

@@ -1,9 +0,0 @@
package main
import (
"nntpchan/cmd/nntpchan"
)
func main() {
nntpchan.Main()
}

View File

@@ -1,160 +0,0 @@
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")
}

View File

@@ -1,42 +0,0 @@
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)
}
}

View File

@@ -1,4 +0,0 @@
//
// server admin panel
//
package admin

View File

@@ -1,16 +0,0 @@
package admin
import (
"net/http"
)
type Server struct {
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func NewServer() *Server {
return &Server{}
}

View File

@@ -1,10 +0,0 @@
package api
import (
"nntpchan/lib/model"
)
// json api
type API interface {
MakePost(p model.Post)
}

View File

@@ -1,2 +0,0 @@
// json api
package api

View File

@@ -1,20 +0,0 @@
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)
}

View File

@@ -1,73 +0,0 @@
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,
}

View File

@@ -1,13 +0,0 @@
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"`
}

View File

@@ -1,87 +0,0 @@
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
}

View File

@@ -1,18 +0,0 @@
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: "",
}

View File

@@ -1,4 +0,0 @@
//
// package for parsing config files
//
package config

View File

@@ -1,33 +0,0 @@
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,
}

View File

@@ -1,21 +0,0 @@
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,
}

View File

@@ -1,15 +0,0 @@
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",
}

View File

@@ -1,14 +0,0 @@
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/",
}

View File

@@ -1,24 +0,0 @@
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: "",
}

View File

@@ -1,13 +0,0 @@
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",
}

View File

@@ -1,11 +0,0 @@
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"`
}

View File

@@ -1,10 +0,0 @@
package config
type StoreConfig struct {
// path to article directory
Path string `json:"path"`
}
var DefaultStoreConfig = StoreConfig{
Path: "storage",
}

View File

@@ -1,17 +0,0 @@
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",
}

View File

@@ -1,5 +0,0 @@
//
// nntpchan crypto package
// wraps all external crypro libs
//
package crypto

View File

@@ -1,8 +0,0 @@
package crypto
import (
"github.com/dchest/blake256"
)
// common hash function is blake2
var Hash = blake256.New

View File

@@ -1,82 +0,0 @@
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
}

View File

@@ -1,95 +0,0 @@
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())
}

View File

@@ -1,86 +0,0 @@
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)
}

View File

@@ -1,178 +0,0 @@
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())
}

View File

@@ -1,44 +0,0 @@
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)
}
}

View File

@@ -1,24 +0,0 @@
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
}

View File

@@ -1,58 +0,0 @@
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
}

View File

@@ -1,342 +0,0 @@
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
}

View File

@@ -1,53 +0,0 @@
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
}

View File

@@ -1,34 +0,0 @@
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()
}
}

View File

@@ -1,8 +0,0 @@
package crypto
import (
"nntpchan/lib/crypto/nacl"
)
// generate random bytes
var RandBytes = nacl.RandBytes

View File

@@ -1,25 +0,0 @@
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)
}

View File

@@ -1,14 +0,0 @@
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
}

View File

@@ -1,14 +0,0 @@
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
}

View File

@@ -1,26 +0,0 @@
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
}

View File

@@ -1,4 +0,0 @@
//
// database driver
//
package database

View File

@@ -1,28 +0,0 @@
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
}

View File

@@ -1,123 +0,0 @@
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)
}
}

View File

@@ -1,5 +0,0 @@
//
// nntpchan frontend
// allows posting to nntpchan network via various implementations
//
package frontend

View File

@@ -1,46 +0,0 @@
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
}

View File

@@ -1,136 +0,0 @@
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
}

View File

@@ -1,14 +0,0 @@
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)
}

View File

@@ -1,115 +0,0 @@
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
}

View File

@@ -1 +0,0 @@
package frontend

View File

@@ -1 +0,0 @@
package frontend

View File

@@ -1,15 +0,0 @@
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
}

View File

@@ -1,10 +0,0 @@
package model
type Attachment struct {
Path string
Name string
Mime string
Hash string
// only filled for api
Body string
}

View File

@@ -1,4 +0,0 @@
package model
type Board struct {
}

View File

@@ -1,8 +0,0 @@
package model
type BoardPage struct {
Name string
Page int
Pages int
Threads []Thread
}

View File

@@ -1,2 +0,0 @@
// MVC models
package model

View File

@@ -1,29 +0,0 @@
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]
}

View File

@@ -1,32 +0,0 @@
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]
}

View File

@@ -1,6 +0,0 @@
package model
type Thread struct {
Root *Post
Replies []*Post
}

View File

@@ -1,37 +0,0 @@
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