diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c1efa2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/i2p-reseeder +/cert.pem +/key.pem +/netdb diff --git a/cert.go b/cert.go new file mode 100644 index 0000000..6d83494 --- /dev/null +++ b/cert.go @@ -0,0 +1,99 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "net" + "os" + "strings" + "time" +) + +func GenerateCert(host string, validFrom string, validFor time.Duration, isCA bool, rsaBits int) { + if len(host) == 0 { + log.Fatalf("Missing required -host parameter") + } + + priv, err := rsa.GenerateKey(rand.Reader, rsaBits) + if err != nil { + log.Fatalf("failed to generate private key: %s", err) + return + } + + var notBefore time.Time + if len(validFrom) == 0 { + notBefore = time.Now() + } else { + notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse creation date: %s\n", err) + return + } + } + + notAfter := notBefore.Add(validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("failed to generate serial number: %s", err) + return + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"I2P"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + hosts := strings.Split(host, ",") + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + if isCA { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + log.Fatalf("Failed to create certificate: %s", err) + return + } + + certOut, err := os.Create("cert.pem") + if err != nil { + log.Fatalf("failed to open cert.pem for writing: %s", err) + return + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() + log.Print("written cert.pem\n") + + keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + log.Print("failed to open key.pem for writing:", err) + return + } + pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + keyOut.Close() + log.Print("written key.pem\n") +} diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..ee88567 --- /dev/null +++ b/cli.go @@ -0,0 +1,94 @@ +package main + +import ( + "github.com/codegangsta/cli" + "os" + "time" +) + +func main() { + app := cli.NewApp() + app.Name = "reseeder" + app.Version = "1.0.0" + app.Usage = "I2P reseed server" + + app.Commands = []cli.Command{ + { + Name: "serve", + ShortName: "s", + Usage: "Start an http server for SU3 files", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "addr", + Value: "", + Usage: "Address to bind to", + }, + cli.StringFlag{ + Name: "port", + Value: "8080", + Usage: "Port to listen on", + }, + cli.StringFlag{ + Name: "cert", + Value: "cert.pem", + Usage: "Certificate for TLS", + }, + cli.StringFlag{ + Name: "key", + Value: "key.pem", + Usage: "Key for TLS certificate", + }, + cli.StringFlag{ + Name: "netdb", + Value: "./netdb", + Usage: "Path to NetDB directory containing routerInfo files", + }, + cli.DurationFlag{ + Name: "refresh", + Value: 300 * time.Second, + Usage: "Period to refresh routerInfo lists in time duration format (200ns, 1s, 5m)", + }, + }, + Action: func(c *cli.Context) { + server := NewReseeder() + server.NetDBDir = c.String("netdb") + server.RefreshInterval = c.Duration("refresh") + server.Start(c.String("addr"), c.String("port"), c.String("cert"), c.String("key")) + }, + }, + { + Name: "generate", + ShortName: "g", + Usage: "Generate a celf-signed certificate", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "host", + Usage: "Comma-separated hostnames and IPs to generate a certificate for", + }, + cli.StringFlag{ + Name: "validFrom", + Usage: "Creation date formatted as Jan 1 15:04:05 2011", + }, + cli.DurationFlag{ + Name: "validFor", + Value: 365 * 24 * time.Hour, + Usage: "Duration that certificate is valid for", + }, + cli.BoolFlag{ + Name: "ca", + Usage: "Whether this cert should be its own Certificate Authority", + }, + cli.IntFlag{ + Name: "rsaBits", + Value: 2048, + Usage: "Size of RSA key to generate", + }, + }, + Action: func(c *cli.Context) { + GenerateCert(c.String("host"), c.String("validFrom"), c.Duration("validFor"), c.Bool("isCA"), c.Int("rsaBits")) + }, + }, + } + + app.Run(os.Args) +} diff --git a/reseeder.go b/reseeder.go index 7627be8..68c4155 100644 --- a/reseeder.go +++ b/reseeder.go @@ -1,142 +1,78 @@ package main +// read in all files from netdb dir into a slice of routerinfos + +// for every unique requesting IP +// look up that IP in the db +// - if it exists, check the creation time +// - if the creation time is within the threshold, serve up the routerinfos +// - if the creation time is outside the threshold, or if it does not exist generate a new slice of routerinfos from the current master set + +// at some regular interval, update the master slice with fresh netdb routerinfos + +// can serve up html/ul of routerinfos +// can serve up su3 signed file +// https://geti2p.net/en/docs/spec/updates + import ( - "flag" "fmt" - "github.com/braintree/manners" "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "html/template" + "io" "io/ioutil" "log" + "math/rand" "net/http" "os" - "os/signal" - "sync" "time" ) -var netdbDir = flag.String("dir", "./netdb", "Location of your netdb directory") -var bindIp = flag.String("ip", "", "Interface to bind to") -var bindPort = flag.String("port", "3000", "Port to bind to") +type Reseeder struct { + NetDBDir string + nextMap chan []string + RefreshInterval time.Duration +} -func main() { - flag.Parse() +func (rs *Reseeder) Start(addr, port, cert, key string) { + log.Println("Starting reseed server on " + addr + ":" + port) - netdb := NewNetDb(*netdbDir) - reseeder := &Reseeder{netdb, make([]*Peer, 100)} + r := mux.NewRouter() + s := r.PathPrefix("/netdb").Subrouter() + s.HandleFunc("/", rs.listHandler) + //s.HandleFunc("/i2pseeds.su3", rs.su3Handler) + s.HandleFunc(`/routerInfo-{hash:[A-Za-z0-9+/\-=~]+}.dat`, rs.routerInfoHandler) - log.Printf("Starting server on %s:%s serving netdb from %s", *bindIp, *bindPort, *netdbDir) + http.Handle("/", handlers.CombinedLoggingHandler(os.Stdout, proxiedHandler(r))) - server := manners.NewServer() + go rs.runMap() + rs.Refresh() - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) + // sample function to update routerInfo map every minute go func() { - <-c - log.Println("waiting for connections to close and exiting...") - server.Shutdown <- true - go time.AfterFunc(time.Duration(5)*time.Second, func() { - log.Println("Killing idle connections...") - os.Exit(0) - }) + for { + time.Sleep(rs.RefreshInterval) + log.Println("Updating routerInfos") + rs.Refresh() + } }() - err := server.ListenAndServe(*bindIp+":"+*bindPort, handlers.CombinedLoggingHandler(os.Stdout, reseeder)) - if err != nil { - log.Fatal("ListenAndServe: ", err) - } -} - -type Reseeder struct { - NetDb *NetDb - Peers []*Peer -} - -func (rs *Reseeder) ServeHTTP(w http.ResponseWriter, req *http.Request) { - infos := make(chan *RouterInfo) - - peer := &Peer{req.RemoteAddr, time.Now(), time.Now()} - - go rs.GetForPeer(peer, infos) - - for info := range infos { - fmt.Fprintf(w, "%s\n", info.Name) - } -} - -func (rs *Reseeder) GetForPeer(peer *Peer, infos chan *RouterInfo) { - rs.NetDb.lock.RLock() - defer rs.NetDb.lock.RUnlock() - - for _, info := range rs.NetDb.RouterInfos { - infos <- info - } - close(infos) -} - -func NewNetDb(dir string) *NetDb { - if _, err := os.Stat(dir); err != nil { - if os.IsNotExist(err) { - log.Fatalf("netdb directory is not readable: %s", dir) - } else { - // other error + if _, err := os.Stat(cert); err == nil { + if _, err := os.Stat(key); err == nil { + err := http.ListenAndServeTLS(addr+":"+port, cert, key, nil) + if nil != err { + log.Fatalln(err) + } + return } } - netdb := &NetDb{Dir: dir, RouterInfos: make(map[string]*RouterInfo, 1000)} - netdb.Refresh() - return netdb -} - -type NetDb struct { - lock sync.RWMutex - RouterInfos map[string]*RouterInfo - Dir string -} - -func (db *NetDb) Refresh() { - files, err := ioutil.ReadDir(db.Dir) + err := http.ListenAndServe(addr+":"+port, nil) if nil != err { - log.Fatalf("unable to read %s", db.Dir) - } - for _, file := range files { - db.Set(file.Name(), NewRouterInfo(db.Dir+file.Name())) + log.Fatalln(err) } } -func (db *NetDb) Get(key string) (*RouterInfo, bool) { - db.lock.RLock() - defer db.lock.RUnlock() - d, ok := db.RouterInfos[key] - return d, ok -} - -func (db *NetDb) Set(key string, d *RouterInfo) { - db.lock.Lock() - defer db.lock.Unlock() - db.RouterInfos[key] = d -} - -func (db *NetDb) UnSet(key string) { - db.lock.Lock() - defer db.lock.Unlock() - delete(db.RouterInfos, key) -} - -func NewRouterInfo(file string) *RouterInfo { - return &RouterInfo{file} -} - -type RouterInfo struct { - Name string -} - -type Peer struct { - Ip string - Seen time.Time - Created time.Time -} - -//// stuff for reverse proxy handling func proxiedHandler(h http.Handler) http.Handler { return remoteAddrFixup{h} } @@ -151,3 +87,74 @@ func (h remoteAddrFixup) ServeHTTP(w http.ResponseWriter, r *http.Request) { } h.h.ServeHTTP(w, r) } + +func NewReseeder() *Reseeder { + return &Reseeder{nextMap: make(chan []string)} +} + +func (r *Reseeder) runMap() { + var m []string + for { + select { + case m = <-r.nextMap: + case r.nextMap <- m: + } + } +} + +func (r *Reseeder) Refresh() { + var m []string + + src, err := ioutil.ReadDir(r.NetDBDir) + if nil != err { + log.Fatalln("error reading netdb dir", err) + return + } + + // randomize the file order + files := make([]os.FileInfo, len(src)) + perm := rand.Perm(len(src)) + for i, v := range perm { + files[v] = src[i] + } + + added := 0 + for _, file := range files { + if !file.IsDir() && file.Name() != "." && file.Name() != ".." { + m = append(m, file.Name()) + added++ + } + if added >= 50 { + break + } + } + + r.nextMap <- m +} + +func (rs *Reseeder) listHandler(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.New("foo").Parse(`