Files
newsgo/main.go
2025-03-21 14:47:52 -04:00

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()
}
}