tapir/notifications/main.go

193 lines
6.0 KiB
Go

package main
import (
"bytes"
"compress/gzip"
"crypto/rand"
"crypto/sha512"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/primitives"
"encoding/hex"
"encoding/json"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"os"
"time"
)
// This example implements a basic notification application which allows peers to notify each other of new messages without downloading
// the entire contents of the server.
// NOTE: Very Incomplete Prototype.
// Notification contains a Topic string and a Message.
type Notification struct {
Topic string // A hex encoded string of the hash of the topic string
Message string
}
// NotificationClient allows publishing and reading from the notifications server
type NotificationClient struct {
applications.AuthApp
connection *tapir.Connection
}
// NewInstance should always return a new instantiation of the application.
func (nc NotificationClient) NewInstance() tapir.Application {
app := new(NotificationClient)
return app
}
// Init is run when the connection is first started.
func (nc *NotificationClient) Init(connection *tapir.Connection) {
// First run the Authentication App
nc.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
nc.connection = connection
}
}
// Publish transforms the given topic string into a hashed ID, and sends the ID along with the message
// NOTE: Server learns the hash of the topic (and therefore can correlate repeated use of the same topic)
func (nc NotificationClient) Publish(topic string, message string) {
log.Debugf("Sending Publish Request")
hashedTopic := sha512.Sum512([]byte(topic))
data, _ := json.Marshal(notificationRequest{RequestType: "Publish", RequestData: map[string]string{"Topic": hex.EncodeToString(hashedTopic[:])}})
nc.connection.Send([]byte(data))
}
// Check returns true if the server might have notifications related to the topic.
// This check reveals nothing about the topic to the server.
func (nc NotificationClient) Check(topic string) bool {
log.Debugf("Sending Filter Request")
// Get an updated bloom filter
data, _ := json.Marshal(notificationRequest{RequestType: "BloomFilter", RequestData: map[string]string{}})
nc.connection.Send(data)
response := nc.connection.Expect()
var bf []primitives.BloomFilter
r, _ := gzip.NewReader(bytes.NewReader(response))
bfb, _ := ioutil.ReadAll(r)
json.Unmarshal(bfb, &bf)
// Check the topic handle in the bloom filter
hashedTopic := sha512.Sum512([]byte(topic))
return bf[time.Now().Hour()].Check(hashedTopic[:])
}
type notificationRequest struct {
RequestType string
RequestData map[string]string
}
// NotificationsServer implements the metadata resistant notifications server
type NotificationsServer struct {
applications.AuthApp
Filter []*primitives.BloomFilter
timeProvider primitives.TimeProvider
}
const DefaultNumberOfBuckets = 24 // 1 per hour of the day
// NewInstance should always return a new instantiation of the application.
func (ns NotificationsServer) NewInstance() tapir.Application {
app := new(NotificationsServer)
app.timeProvider = new(primitives.OSTimeProvider)
app.Filter = make([]*primitives.BloomFilter, DefaultNumberOfBuckets)
for i := range app.Filter {
app.Filter[i] = new(primitives.BloomFilter)
app.Filter[i].Init(1024)
}
return app
}
// Configure overrides the default parameters for the Notification Server
func (ns NotificationsServer) Configure(timeProvider primitives.TimeProvider) {
ns.timeProvider = timeProvider
}
// Init initializes the application.
func (ns NotificationsServer) Init(connection *tapir.Connection) {
// First run the Authentication App
ns.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
for {
request := connection.Expect()
var nr notificationRequest
json.Unmarshal(request, &nr)
log.Debugf("Received Request %v", nr)
switch nr.RequestType {
case "Publish":
log.Debugf("Received Publish Request")
topic := nr.RequestData["Topic"]
// message := nr.RequestData["Message"]
topicID, err := hex.DecodeString(topic)
if err == nil {
currentBucket := ns.timeProvider.GetCurrentTime().Hour()
ns.Filter[currentBucket].Insert(topicID)
}
case "BloomFilter":
log.Debugf("Received Filter Request")
response, _ := json.Marshal(ns.Filter)
var b bytes.Buffer
w := gzip.NewWriter(&b)
w.Write(response)
w.Close()
connection.Send(b.Bytes())
}
}
}
}
func main() {
log.SetLevel(log.LevelDebug)
// Connect to Tor
var acn connectivity.ACN
acn, _ = connectivity.StartTor("./", "")
acn.WaitTillBootstrapped()
// Generate Server Keys
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
id := identity.InitializeV3("server", &sk, &pk)
// Init a Client to Connect to the Server
go client(acn, pubkey)
rm
}
// Client will Connect and launch it's own Echo App goroutine.
func client(acn connectivity.ACN, key ed25519.PublicKey) {
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
id := identity.InitializeV3("client", &sk, &pk)
var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(acn, sk, id)
cid, _ := client.Connect(utils.GetTorV3Hostname(key), new(NotificationClient))
conn, err := client.WaitForCapabilityOrClose(cid, applications.AuthCapability)
if err == nil {
log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability))
nc := conn.App.(*NotificationClient)
// Basic Demonstration of Notification
log.Infof("Publishing to #astronomy: %v", nc.Check("#astronomy"))
nc.Publish("#astronomy", "New #Astronomy Post!")
log.Infof("Checking #astronomy: %v", nc.Check("#astronomy"))
}
os.Exit(0)
}