Archived
1
0
This repository has been archived on 2023-08-12. You can view files and clone it, but cannot push or open issues or pull requests.
nntpchan/contrib/backends/srndv2/src/srnd/util.go

700 lines
15 KiB
Go
Raw Normal View History

//
// util.go -- various utilities
//
package srnd
import (
"bufio"
2017-08-25 18:30:34 +05:00
"crypto/rand"
"crypto/sha1"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"fmt"
2017-08-25 17:52:41 +05:00
"golang.org/x/crypto/ed25519"
"io"
"log"
"net"
"net/http"
"net/mail"
"net/textproto"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
func DelFile(fname string) {
if CheckFile(fname) {
os.Remove(fname)
}
}
func CheckFile(fname string) bool {
if _, err := os.Stat(fname); os.IsNotExist(err) {
return false
}
return true
}
func IsDir(dirname string) bool {
stat, err := os.Stat(dirname)
if err != nil {
log.Fatal(err)
}
return stat.IsDir()
}
// ensure a directory exists
func EnsureDir(dirname string) {
stat, err := os.Stat(dirname)
if os.IsNotExist(err) {
os.Mkdir(dirname, 0755)
} else if !stat.IsDir() {
os.Remove(dirname)
os.Mkdir(dirname, 0755)
}
}
var exp_valid_message_id = regexp.MustCompilePOSIX(`^<[a-zA-Z0-9$.]{2,128}@[a-zA-Z0-9\-.]{2,63}>$`)
func ValidMessageID(id string) bool {
return exp_valid_message_id.MatchString(id)
}
// message id hash
func HashMessageID(msgid string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(msgid)))
}
// short message id hash
func ShortHashMessageID(msgid string) string {
return strings.ToLower(HashMessageID(msgid)[:18])
}
// will this message id produce quads?
func MessageIDWillDoQuads(msgid string) bool {
h := HashMessageID(msgid)
return h[0] == h[1] && h[1] == h[2] && h[2] == h[3]
}
// will this message id produce trips?
func MessageIDWillDoTrips(msgid string) bool {
h := HashMessageID(msgid)
return h[0] == h[1] && h[1] == h[2]
}
// will this message id produce dubs?
func MessageIDWillDoDubs(msgid string) bool {
h := HashMessageID(msgid)
return h[0] == h[1]
}
// shorter message id hash
func ShorterHashMessageID(msgid string) string {
return strings.ToLower(HashMessageID(msgid)[:10])
}
func OpenFileWriter(fname string) (io.WriteCloser, error) {
return os.Create(fname)
}
2017-08-25 18:30:34 +05:00
func randbytes(l int) []byte {
b := make([]byte, l)
io.ReadFull(rand.Reader, b)
return b
}
// make a random string
func randStr(length int) string {
2017-08-25 18:30:34 +05:00
return hex.EncodeToString(randbytes(length))[length:]
}
// time for right now as int64
func timeNow() int64 {
return time.Now().UTC().Unix()
}
// sanitize data for nntp
func nntpSanitize(data string) (ret string) {
parts := strings.Split(data, "\n")
lines := len(parts)
for idx, part := range parts {
part = strings.Replace(part, "\n", "", -1)
part = strings.Replace(part, "\r", "", -1)
if part == "." {
part = " ."
}
ret += part
if idx+1 < lines {
ret += "\n"
}
}
return ret
}
type int64Sorter []int64
func (self int64Sorter) Len() int {
return len(self)
}
func (self int64Sorter) Less(i, j int) bool {
return self[i] < self[j]
}
func (self int64Sorter) Swap(i, j int) {
tmp := self[j]
self[j] = self[i]
self[i] = tmp
}
// obtain the "real" ip address
func getRealIP(name string) string {
if len(name) > 0 {
ip, err := net.ResolveIPAddr("ip", name)
if err == nil {
if ip.IP.IsGlobalUnicast() {
return ip.IP.String()
}
}
}
return ""
}
// check that we have permission to access this
// fatal on fail
func checkPerms(fname string) {
fstat, err := os.Stat(fname)
if err != nil {
log.Fatalf("Cannot access %s, %s", fname, err)
}
// check if we can access this dir
if fstat.IsDir() {
tmpfname := filepath.Join(fname, ".test")
f, err := os.Create(tmpfname)
if err != nil {
log.Fatalf("No Write access in %s, %s", fname, err)
}
err = f.Close()
if err != nil {
log.Fatalf("failed to close test file %s !? %s", tmpfname, err)
}
err = os.Remove(tmpfname)
if err != nil {
log.Fatalf("failed to remove test file %s, %s", tmpfname, err)
}
} else {
// this isn't a dir, treat it like a regular file
f, err := os.Open(fname)
if err != nil {
log.Fatalf("cannot read file %s, %s", fname, err)
}
f.Close()
}
}
// number of bytes to use in otp
func encAddrBytes() int {
return 64
}
// length of an encrypted clearnet address
func encAddrLen() int {
return 88
}
// length of an i2p dest hash
func i2pDestHashLen() int {
return 44
}
// given an address
// generate a new encryption key for it
// return the encryption key and the encrypted address
func newAddrEnc(addr string) (string, string) {
2017-08-25 18:30:34 +05:00
key_bytes := randbytes(encAddrBytes())
key := base64.StdEncoding.EncodeToString(key_bytes)
return key, encAddr(addr, key)
}
// xor address with a one time pad
// if the address isn't long enough it's padded with spaces
func encAddr(addr, key string) string {
key_bytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
log.Println("encAddr() key base64 decode", err)
return ""
}
if len(addr) > len(key_bytes) {
log.Println("encAddr() len(addr) > len(key_bytes)")
return ""
}
// pad with spaces
for len(addr) < len(key_bytes) {
addr += " "
}
addr_bytes := []byte(addr)
res_bytes := make([]byte, len(addr_bytes))
for idx, b := range key_bytes {
res_bytes[idx] = addr_bytes[idx] ^ b
}
return base64.StdEncoding.EncodeToString(res_bytes)
}
func checkError(err error) {
if err != nil {
log.Fatal(err)
}
}
// decrypt an address
// strips any whitespaces
func decAddr(encaddr, key string) string {
encaddr_bytes, err := base64.StdEncoding.DecodeString(encaddr)
if err != nil {
log.Println("decAddr() encaddr base64 decode", err)
return ""
}
if len(encaddr) != len(key) {
log.Println("decAddr() len(encaddr_bytes) != len(key)")
return ""
}
key_bytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
log.Println("decAddr() key base64 decode", err)
}
res_bytes := make([]byte, len(key))
for idx, b := range key_bytes {
res_bytes[idx] = encaddr_bytes[idx] ^ b
}
res := string(res_bytes)
2017-08-04 01:23:34 +05:00
res = strings.Trim(res, " ")
if strings.Index(res, "/") == -1 {
// TODO: ipv6
res += "/32"
}
return res
}
var exp_valid_newsgroup = regexp.MustCompilePOSIX(`^[a-zA-Z0-9.]{1,128}$`)
func newsgroupValidFormat(newsgroup string) bool {
return exp_valid_newsgroup.MatchString(newsgroup)
}
func ValidNewsgroup(newsgroup string) bool {
return newsgroupValidFormat(newsgroup)
}
2017-08-25 18:30:34 +05:00
func genKeypair() (pk, sk []byte) {
sk = randbytes(32)
pk, _ = seedToKeyPair(sk)
return
}
// generate a new signing keypair
// public, secret
func newSignKeypair() (string, string) {
2017-08-25 18:30:34 +05:00
pk, sk := genKeypair()
return hex.EncodeToString(pk), hex.EncodeToString(sk)
}
// make a utf-8 tripcode
func makeTripcode(pk string) string {
data, err := hex.DecodeString(pk)
if err == nil {
tripcode := ""
// here is the python code this is based off of
// i do something slightly different but this is the base
//
// for x in range(0, length / 2):
// pub_short += '&#%i;' % (9600 + int(full_pubkey_hex[x*2:x*2+2], 16))
// length -= length / 2
// for x in range(0, length):
// pub_short += '&#%i;' % (9600 + int(full_pubkey_hex[-(length*2):][x*2:x*2+2], 16))
//
for _, c := range data {
ch := 9600
ch += int(c)
tripcode += fmt.Sprintf("&#%04d;", ch)
}
return tripcode
}
return "[invalid]"
}
// generate a new message id with base name
func genMessageID(name string) string {
return fmt.Sprintf("<%s%d@%s>", randStr(5), timeNow(), name)
}
// time now as a string timestamp
func timeNowStr() string {
return time.Unix(timeNow(), 0).UTC().Format(time.RFC1123Z)
}
func queryGetInt64(q url.Values, key string, fallback int64) int64 {
val := q.Get(key)
if val != "" {
i, err := strconv.ParseInt(val, 10, 64)
if err == nil {
return i
}
}
return fallback
}
// get from a map an int given a key or fall back to a default value
func mapGetInt(m map[string]string, key string, fallback int) int {
val, ok := m[key]
if ok {
i, err := strconv.ParseInt(val, 10, 32)
if err == nil {
return int(i)
}
}
return fallback
}
2017-04-04 19:31:41 +05:00
// get from a map an uint64 given a key or fall back to a default value
func mapGetInt64(m map[string]string, key string, fallback int64) int64 {
val, ok := m[key]
if ok {
i, err := strconv.ParseInt(val, 10, 64)
if err == nil {
return i
}
}
return fallback
}
func isSage(str string) bool {
str = strings.ToLower(str)
return str == "sage" || strings.HasPrefix(str, "sage ")
}
func unhex(str string) []byte {
buff, _ := hex.DecodeString(str)
return buff
}
func hexify(data []byte) string {
return hex.EncodeToString(data)
}
// extract pubkey from secret key
// return as hex
func getSignPubkey(sk []byte) string {
2017-08-25 18:30:34 +05:00
_, pk := seedToKeyPair(sk)
return hexify(pk)
}
// sign data with secret key the fucky srnd way
// return signature as hex
2017-08-25 18:30:34 +05:00
// XXX: DEPRECATED
func cryptoSignFucky(h, sk []byte) string {
// sign
2017-08-25 18:37:05 +05:00
sig := naclCryptoSignFucky(h, sk)
if sig == nil {
return "[failed to sign]"
}
return hexify(sig)
}
2017-08-25 17:52:41 +05:00
// convert seed to secret key
2017-08-25 18:37:05 +05:00
func seedToKeyPair(seed []byte) (pub ed25519.PublicKey, full ed25519.PrivateKey) {
pub = make(ed25519.PublicKey, ed25519.PublicKeySize)
full = make(ed25519.PrivateKey, ed25519.PrivateKeySize)
2017-08-25 17:52:41 +05:00
var in [32]byte
2017-08-25 18:30:34 +05:00
var out [32]byte
2017-08-25 17:52:41 +05:00
copy(in[:], seed[0:32])
2017-08-25 19:11:23 +05:00
naclScalarBaseMult(&out, &in)
2017-08-25 18:30:34 +05:00
copy(pub[:], out[:])
2017-08-25 19:11:23 +05:00
copy(full[0:32], in[:])
copy(full[32:64], pub[:])
log.Println("pk=", hexify(pub[:]))
2017-08-25 17:52:41 +05:00
return
}
2017-08-25 18:30:34 +05:00
func cryptoSignProper(h, sk []byte) string {
2017-08-25 17:52:41 +05:00
// convert key
2017-08-25 18:37:05 +05:00
_, key := seedToKeyPair(sk)
2017-08-25 17:20:06 +05:00
// sign
2017-08-25 17:52:41 +05:00
sig := ed25519.Sign(key, h)
2017-08-25 17:20:06 +05:00
if sig == nil {
return "[failed to sign]"
}
return hexify(sig)
}
// given a tripcode after the #
// make a seed byteslice
func parseTripcodeSecret(str string) []byte {
// try decoding hex
raw := unhex(str)
2017-08-25 18:30:34 +05:00
keylen := 32
if raw == nil || len(raw) != keylen {
// treat this as a "regular" chan tripcode
// decode as bytes then pad the rest with 0s if it doesn't fit
raw = make([]byte, keylen)
str_bytes := []byte(str)
if len(str_bytes) > keylen {
copy(raw, str_bytes[:keylen])
} else {
copy(raw, str_bytes)
}
}
return raw
}
// generate a login salt for nntp users
func genLoginCredSalt() (salt string) {
salt = randStr(128)
return
}
// do nntp login credential hash given password and salt
func nntpLoginCredHash(passwd, salt string) (str string) {
var b []byte
b = append(b, []byte(passwd)...)
b = append(b, []byte(salt)...)
h := sha512.Sum512(b)
str = base64.StdEncoding.EncodeToString(h[:])
return
}
func IsSubnet(cidr string) (bool, *net.IPNet) {
_, ipnet, err := net.ParseCIDR(cidr)
if err == nil {
return true, ipnet
}
return false, nil
}
func IPNet2MinMax(inet *net.IPNet) (min, max net.IP) {
netb := []byte(inet.IP)
maskb := []byte(inet.Mask)
maxb := make([]byte, len(netb))
for i, _ := range maxb {
maxb[i] = netb[i] | (^maskb[i])
}
min = net.IP(netb)
max = net.IP(maxb)
return
}
func ZeroIPString(ip net.IP) string {
p := ip
if len(ip) == 0 {
return "<nil>"
}
if p4 := p.To4(); len(p4) == net.IPv4len {
return fmt.Sprintf("%03d.%03d.%03d.%03d", p4[0], p4[1], p4[2], p4[3])
}
if len(p) == net.IPv6len {
//>IPv6
//ishygddt
return fmt.Sprintf("[%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x]", p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[10], p[11], p[12], p[13], p[14], p[15])
}
return "?"
}
func getThreadHash(file string) (thread string) {
exp := regexp.MustCompilePOSIX(`thread-([0-9a-f]+)\.*`)
matches := exp.FindStringSubmatch(file)
if len(matches) != 2 {
return ""
}
thread = matches[1]
return
}
func getUkkoPage(file string) (page int) {
exp := regexp.MustCompilePOSIX(`ukko-([0-9]+)\.*`)
matches := exp.FindStringSubmatch(file)
if len(matches) != 2 {
return
}
var err error
page, err = strconv.Atoi(matches[1])
if err != nil {
page = 0
}
return
}
func getGroupAndPage(file string) (board string, page int) {
exp := regexp.MustCompilePOSIX(`(.*)-([0-9]+)\.*`)
matches := exp.FindStringSubmatch(file)
if len(matches) != 3 {
return "", -1
}
var err error
board = matches[1]
tmp := matches[2]
page, err = strconv.Atoi(tmp)
if err != nil {
page = -1
}
return
}
func getGroupForCatalog(file string) (group string) {
exp := regexp.MustCompilePOSIX(`catalog-(.+)\.html`)
matches := exp.FindStringSubmatch(file)
if len(matches) != 2 {
return ""
}
group = matches[1]
return
}
// get a message id from a mime header
// checks many values
func getMessageID(hdr textproto.MIMEHeader) (msgid string) {
msgid = hdr.Get("Message-Id")
if msgid == "" {
msgid = hdr.Get("Message-ID")
}
if msgid == "" {
msgid = hdr.Get("message-id")
}
if msgid == "" {
msgid = hdr.Get("MESSAGE-ID")
}
return
}
func getMessageIDFromArticleHeaders(hdr ArticleHeaders) (msgid string) {
msgid = hdr.Get("Message-Id", hdr.Get("Message-ID", hdr.Get("message-id", hdr.Get("MESSAGE-ID", ""))))
return
}
func readMIMEHeader(r *bufio.Reader) (msg *mail.Message, err error) {
msg, err = mail.ReadMessage(r)
/*
hdr = make(textproto.MIMEHeader)
for {
var str string
str, err = r.ReadString(10)
if err != nil {
hdr = nil
return
}
str = strings.Trim(str, "\r")
str = strings.Trim(str, "\n")
if str == "" {
break
}
idx := strings.Index(str, ": ")
if idx > 0 {
hdrname := strings.Trim(str[:idx], " ")
hdrval := strings.Trim(str[idx+2:], "\r\n")
hdr.Add(hdrname, hdrval)
} else {
log.Println("invalid header", str)
}
}
*/
return
}
// write out a mime header to a writer
func writeMIMEHeader(wr io.Writer, hdr map[string][]string) (err error) {
// write headers
for k, vals := range hdr {
for _, val := range vals {
wr.Write([]byte(k))
wr.Write([]byte(": "))
wr.Write([]byte(val))
_, err = wr.Write([]byte{10})
}
}
// end of headers
_, err = wr.Write([]byte{10})
return
}
// like ioutil.Discard but an io.WriteCloser
type discardCloser struct {
}
func (*discardCloser) Write(data []byte) (n int, err error) {
n = len(data)
return
}
func (*discardCloser) Close() (err error) {
return
}
// like ioutil.Discard but an io.WriteCloser
var Discard = new(discardCloser)
func extractParamFallback(param map[string]interface{}, k, fallback string) string {
v, ok := param[k]
if ok {
return v.(string)
}
return fallback
}
func extractParam(param map[string]interface{}, k string) string {
return extractParamFallback(param, k, "")
}
// get real ip addresss from an http request
func extractRealIP(r *http.Request) (ip string, err error) {
ip, _, err = net.SplitHostPort(r.RemoteAddr)
if err != nil {
log.Println("extract real ip: ", err)
}
// TODO: have in config upstream proxy ip and check for that
if strings.HasPrefix(ip, "127.") {
// if it's loopback check headers for reverse proxy headers
// TODO: make sure this isn't a tor user being sneaky
ip = getRealIP(r.Header.Get("X-Real-IP"))
if ip == "" {
// try X-Forwarded-For if X-Real-IP not set
_ip := r.Header.Get("X-Forwarded-For")
parts := strings.Split(_ip, ",")
_ip = parts[0]
ip = getRealIP(_ip)
}
if ip == "" {
ip = "127.0.0.1"
}
}
return
}
func serverPubkeyIsValid(pubkey string) bool {
b := unhex(pubkey)
2017-08-25 18:30:34 +05:00
return b != nil && len(b) == 32
}
func verifyFrontendSig(pubkey, sig, msgid string) bool {
s := unhex(sig)
k := unhex(pubkey)
h := sha512.Sum512([]byte(msgid))
2017-08-25 18:37:05 +05:00
return naclCryptoVerifyFucky(h[:], s, k)
}
func msgidFrontendSign(sk []byte, msgid string) string {
h := sha512.Sum512([]byte(msgid))
2017-08-25 18:30:34 +05:00
return cryptoSignFucky(h[:], sk)
}