mirror of
https://github.com/go-i2p/newsgo.git
synced 2025-06-09 10:44:11 -04:00
302 lines
8.9 KiB
Go
302 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-i2p/onramp"
|
|
"github.com/google/uuid"
|
|
builder "github.com/go-i2p/newsgo/builder"
|
|
server "github.com/go-i2p/newsgo/server"
|
|
signer "github.com/go-i2p/newsgo/signer"
|
|
)
|
|
|
|
func loadPrivateKey(path string) (*rsa.PrivateKey, error) {
|
|
privPem, err := ioutil.ReadFile(path)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
|
|
privDer, _ := pem.Decode(privPem)
|
|
privKey, err := x509.ParsePKCS1PrivateKey(privDer.Bytes)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
|
|
return privKey, nil
|
|
}
|
|
|
|
func Sign(xmlfeed string) error {
|
|
sk, err := loadPrivateKey(*signingkey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
signer := signer.NewsSigner{
|
|
SignerID: *signerid,
|
|
SigningKey: sk,
|
|
}
|
|
return signer.CreateSu3(xmlfeed)
|
|
}
|
|
|
|
var (
|
|
serve = flag.String("command", "help", "command to run(may be `serve`,`build`,`sign`, or `help`(default)")
|
|
dir = flag.String("newsdir", "build", "directory to serve news from")
|
|
statsfile = flag.String("statsfile", "build/stats.json", "file to store stats in")
|
|
host = flag.String("host", "127.0.0.1", "host to serve on")
|
|
port = flag.String("port", "9696", "port to serve on")
|
|
i2p = flag.Bool("i2p", isSamAround(), "automatically co-host on an I2P service using SAMv3")
|
|
tcp = flag.Bool("http", true, "host on an HTTP service at host:port")
|
|
//newsfile = flag.String("newsfile", "data/entries.html", "entries to pass to news generator. If passed a directory, all `entries.html` files in the directory will be processed")
|
|
newsfile = flag.String("newsfile", "data", "entries to pass to news generator. If passed a directory, all `entries.html` files in the directory will be processed")
|
|
bloclist = flag.String("blocklist", "data/blocklist.xml", "block list file to pass to news generator")
|
|
releasejson = flag.String("releasejson", "data/releases.json", "json file describing an update to pass to news generator")
|
|
title = flag.String("feedtitle", "I2P News", "title to use for the RSS feed to pass to news generator")
|
|
subtitle = flag.String("feedsubtitle", "News feed, and router updates", "subtitle to use for the RSS feed to pass to news generator")
|
|
site = flag.String("feedsite", "http://i2p-projekt.i2p", "site for the RSS feed to pass to news generator")
|
|
mainurl = flag.String("feedmain", DefaultFeedURL(), "Primary newsfeed for updates to pass to news generator")
|
|
backupurl = flag.String("feedbackup", "http://dn3tvalnjz432qkqsvpfdqrwpqkw3ye4n4i2uyfr4jexvo3sp5ka.b32.i2p/news/news.atom.xml", "Backup newsfeed for updates to pass to news generator")
|
|
urn = flag.String("feeduid", uuid.New().String(), "UUID to use for the RSS feed to pass to news generator")
|
|
builddir = flag.String("builddir", "build", "Build directory to output feeds to.")
|
|
signerid = flag.String("signerid", "null@example.i2p", "ID to use when signing the news")
|
|
signingkey = flag.String("signingkey", "signing_key.pem", "Path to a signing key")
|
|
)
|
|
|
|
func validatePort(s *string) {
|
|
_, err := strconv.Atoi(*s)
|
|
if err != nil {
|
|
log.Println("Port is invalid")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func validateCommand(s *string) string {
|
|
switch *s {
|
|
case "serve":
|
|
return "serve"
|
|
case "build":
|
|
return "build"
|
|
case "sign":
|
|
return "sign"
|
|
default:
|
|
return "help"
|
|
}
|
|
}
|
|
|
|
func Help() {
|
|
fmt.Println("newsgo")
|
|
fmt.Println("======")
|
|
fmt.Println("")
|
|
fmt.Println("I2P News Server Tool/Library. A whole lot faster than the python one. Otherwise compatible.")
|
|
fmt.Println("")
|
|
fmt.Println("Usage")
|
|
fmt.Println("-----")
|
|
fmt.Println("")
|
|
fmt.Println("./newsgo -command $command -newsdir $news_directory -statsfile $news_stats_file")
|
|
fmt.Println("")
|
|
fmt.Println("### Commands")
|
|
fmt.Println("")
|
|
fmt.Println(" - serve: Serve newsfeeds from a directory")
|
|
fmt.Println(" - build: Build newsfeeds from XML")
|
|
fmt.Println(" - sign: Sign newsfeeds with local keys")
|
|
fmt.Println("")
|
|
fmt.Println("### Options")
|
|
fmt.Println("")
|
|
fmt.Println("Use these options to configure the software")
|
|
fmt.Println("")
|
|
fmt.Println("#### Server Options(use with `serve`)")
|
|
fmt.Println("")
|
|
fmt.Println(" - `-newsdir`: directory to serve newsfeed from")
|
|
fmt.Println(" - `-statsfile`: file to store the stats in, in json format")
|
|
fmt.Println(" - `-host`: host to serve news files on")
|
|
fmt.Println(" - `-port`: port to serve news files on")
|
|
fmt.Println(" - `-http`: serve news on host:port using HTTP")
|
|
fmt.Println(" - `-i2p`: serve news files directly to I2P using SAMv3")
|
|
fmt.Println("")
|
|
fmt.Println("#### Builder Options(use with `build`)")
|
|
fmt.Println("")
|
|
fmt.Println(" - `-newsfile`: entries to pass to news generator. If passed a directory, all `entries.html` files in the directory will be processed")
|
|
fmt.Println(" - `-blockfile`: block list file to pass to news generator")
|
|
fmt.Println(" - `-releasejson`: json file describing an update to pass to news generator")
|
|
fmt.Println(" - `-feedtitle`: title to use for the RSS feed to pass to news generator")
|
|
fmt.Println(" - `-feedsubtitle`: subtitle to use for the RSS feed to pass to news generator")
|
|
fmt.Println(" - `-feedsite`: site for the RSS feed to pass to news generator")
|
|
fmt.Println(" - `-feedmain`: Primary newsfeed for updates to pass to news generator")
|
|
fmt.Println(" - `-feedbackup`: Backup newsfeed for updates to pass to news generator")
|
|
fmt.Println(" - `-feeduri`: UUID to use for the RSS feed to pass to news generator")
|
|
fmt.Println(" - `-builddir`: directory to output XML files in")
|
|
fmt.Println("")
|
|
fmt.Println("#### Signer Options(use with `sign`)")
|
|
fmt.Println("")
|
|
fmt.Println(" - `-signerid`: ID of the news signer")
|
|
fmt.Println(" - `-signingkey`: path to the signing key")
|
|
}
|
|
|
|
func Serve(host, port string, s *server.NewsServer) error {
|
|
ln, err := net.Listen("tcp", net.JoinHostPort(host, port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return http.Serve(ln, s)
|
|
}
|
|
|
|
func ServeI2P(s *server.NewsServer) error {
|
|
garlic := &onramp.Garlic{}
|
|
defer garlic.Close()
|
|
ln, err := garlic.Listen()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ln.Close()
|
|
return http.Serve(ln, s)
|
|
}
|
|
|
|
func isSamAround() bool {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:7656")
|
|
if err != nil {
|
|
return true
|
|
}
|
|
ln.Close()
|
|
return false
|
|
}
|
|
|
|
func DefaultFeedURL() string {
|
|
if !isSamAround() {
|
|
return "http://tc73n4kivdroccekirco7rhgxdg5f3cjvbaapabupeyzrqwv5guq.b32.i2p/news.atom.xml"
|
|
}
|
|
garlic := &onramp.Garlic{}
|
|
defer garlic.Close()
|
|
ln, err := garlic.Listen()
|
|
if err != nil {
|
|
return "http://tc73n4kivdroccekirco7rhgxdg5f3cjvbaapabupeyzrqwv5guq.b32.i2p/news.atom.xml"
|
|
}
|
|
defer ln.Close()
|
|
return "http://" + ln.Addr().String() + "/news.atom.xml"
|
|
}
|
|
|
|
func Build(newsFile string) {
|
|
news := builder.Builder(newsFile, *releasejson, *bloclist)
|
|
news.TITLE = *title
|
|
news.SITEURL = *site
|
|
news.MAINFEED = *mainurl
|
|
news.BACKUPFEED = *backupurl
|
|
news.SUBTITLE = *subtitle
|
|
news.URNID = *urn
|
|
base := filepath.Join(*newsfile, "entries.html")
|
|
if newsFile != base {
|
|
news.Feed.BaseEntriesHTMLPath = base
|
|
}
|
|
if feed, err := news.Build(); err != nil {
|
|
log.Printf("Build error: %s", err)
|
|
} else {
|
|
filename := strings.Replace(strings.Replace(strings.Replace(strings.Replace(newsFile, ".html", ".atom.xml", -1), "entries.", "news_", -1), "translations", "", -1), "news_atom", "news.atom", -1)
|
|
if err := os.MkdirAll(filepath.Join(*builddir, filepath.Dir(filename)), 0755); err != nil {
|
|
panic(err)
|
|
}
|
|
if err = ioutil.WriteFile(filepath.Join(*builddir, filename), []byte(feed), 0644); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
command := validateCommand(serve)
|
|
validatePort(port)
|
|
switch command {
|
|
case "serve":
|
|
s := server.Serve(*dir, *statsfile)
|
|
if *tcp {
|
|
go func() {
|
|
if err := Serve(*host, *port, s); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
}
|
|
if *i2p {
|
|
go func() {
|
|
if err := ServeI2P(s); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
}
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt)
|
|
go func() {
|
|
for sig := range c {
|
|
log.Println("captured: ", sig)
|
|
s.Stats.Save()
|
|
os.Exit(0)
|
|
}
|
|
}()
|
|
i := 0
|
|
for {
|
|
time.Sleep(time.Minute)
|
|
log.Printf("Running for %d minutes.", i)
|
|
i++
|
|
}
|
|
case "build":
|
|
f, e := os.Stat(*newsfile)
|
|
if e != nil {
|
|
panic(e)
|
|
}
|
|
if f.IsDir() {
|
|
err := filepath.Walk(*newsfile,
|
|
func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ext := filepath.Ext(path)
|
|
if ext == ".html" {
|
|
Build(path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
} else {
|
|
Build(*newsfile)
|
|
}
|
|
case "sign":
|
|
f, e := os.Stat(*newsfile)
|
|
if e != nil {
|
|
panic(e)
|
|
}
|
|
if f.IsDir() {
|
|
err := filepath.Walk(*newsfile,
|
|
func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ext := filepath.Ext(path)
|
|
if ext == ".html" {
|
|
Sign(path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
} else {
|
|
Sign(*newsfile)
|
|
}
|
|
case "help":
|
|
Help()
|
|
default:
|
|
Help()
|
|
}
|
|
}
|