summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2025-06-09 16:57:34 +0200
committerGitHub <noreply@github.com>2025-06-09 16:57:34 +0200
commitd469d0d628f26a61de6c3701f3218db44560a89e (patch)
tree69cf2e08ec32166a980af7bbbd0db646f1b74c67
parentf4b3251b9646c0f733d7cb872523c00476d5ac23 (diff)
downloadonion-newsreader-d469d0d628f26a61de6c3701f3218db44560a89e.tar.gz
onion-newsreader-d469d0d628f26a61de6c3701f3218db44560a89e.tar.xz
onion-newsreader-d469d0d628f26a61de6c3701f3218db44560a89e.zip
Update onion-newsreader.go
-rw-r--r--onion-newsreader.go2802
1 files changed, 2802 insertions, 0 deletions
diff --git a/onion-newsreader.go b/onion-newsreader.go
index 8b13789..f61f25d 100644
--- a/onion-newsreader.go
+++ b/onion-newsreader.go
@@ -1 +1,2803 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "crypto/rand"
+ "fmt"
+ "html"
+ "html/template"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+type NewsgroupInfo struct {
+ Name string
+ High int
+ Low int
+ Status string
+ ArticleCount int
+}
+
+type ArticleOverview struct {
+ Number int
+ Subject string
+ From string
+ Date string
+ MessageID string
+ References string
+ ByteCount int
+ LineCount int
+}
+
+type NNTPClient struct {
+ conn net.Conn
+ reader *bufio.Reader
+ mutex sync.Mutex
+ lastUsed time.Time
+ isConnected bool
+ isConnecting bool
+ connectionChan chan struct{}
+ backoffCount int
+ maxBackoff time.Duration
+ connectedServer string
+ isOnionService bool
+}
+
+type NewsServer struct {
+ client *NNTPClient
+ groups map[string]*NewsgroupInfo
+ mutex sync.RWMutex
+ session string
+ features map[string]bool
+ downloadStatus struct {
+ IsDownloading bool
+ StartTime time.Time
+ LastError string
+ Completed bool
+ GroupCount int
+ mutex sync.RWMutex
+ }
+}
+
+func NewNNTPClient() *NNTPClient {
+ return &NNTPClient{
+ connectionChan: make(chan struct{}, 1),
+ maxBackoff: 5 * time.Minute,
+ }
+}
+
+func NewNewsServer() *NewsServer {
+ sessionBytes := make([]byte, 8)
+ rand.Read(sessionBytes)
+
+ return &NewsServer{
+ client: NewNNTPClient(),
+ groups: make(map[string]*NewsgroupInfo),
+ session: fmt.Sprintf("%x", sessionBytes),
+ features: map[string]bool{
+ "MODE_READER": true,
+ "XOVER": true,
+ "OVER": true,
+ "HDR": true,
+ "LIST_ACTIVE": true,
+ "LIST_NEWSGROUPS": true,
+ },
+ }
+}
+
+func (c *NNTPClient) connectInternal() error {
+ proxyAddr := "127.0.0.1:9050"
+
+ targetConfigs := []struct {
+ addr string
+ isOnion bool
+ description string
+ }{
+ {
+ addr: "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119",
+ isOnion: true,
+ description: "Primary NNTP Onion Service",
+ },
+ {
+ addr: "news.tcpreset.net:119",
+ isOnion: false,
+ description: "TCPReset NNTP Server (via Tor)",
+ },
+ }
+
+ var lastErr error
+ for i, config := range targetConfigs {
+ log.Printf("๐Ÿ”— Attempt %d/%d: %s - %s", i+1, len(targetConfigs), config.description, config.addr)
+
+ var conn net.Conn
+ var err error
+
+ if config.isOnion {
+ conn, err = c.connectViaSocks5(proxyAddr, config.addr)
+ } else {
+ conn, err = c.connectViaSocks5(proxyAddr, config.addr)
+ }
+
+ if err != nil {
+ lastErr = fmt.Errorf("%s failed: %v", config.description, err)
+ log.Printf("โŒ %s", lastErr)
+ continue
+ }
+
+ log.Printf("โœ… Successfully connected to %s", config.description)
+ c.conn = conn
+ c.reader = bufio.NewReader(c.conn)
+ c.isConnected = true
+ c.lastUsed = time.Now()
+ c.connectedServer = config.description
+ c.isOnionService = config.isOnion
+
+ log.Printf("๐Ÿ“ฅ Reading NNTP welcome message from %s...", config.description)
+ c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
+ welcome, err := c.reader.ReadString('\n')
+ c.conn.SetReadDeadline(time.Time{})
+
+ if err != nil {
+ c.isConnected = false
+ lastErr = fmt.Errorf("failed to read welcome from %s: %v", config.description, err)
+ log.Printf("โŒ %s", lastErr)
+ c.conn.Close()
+ c.connectedServer = ""
+ c.isOnionService = false
+ continue
+ }
+
+ welcome = strings.TrimSpace(welcome)
+ log.Printf("๐Ÿ“ฅ Welcome received from %s: %s", config.description, welcome)
+
+ log.Printf("๐Ÿ“ค Sending MODE READER command to %s...", config.description)
+ return c.modeReaderInternal()
+ }
+
+ return fmt.Errorf("failed to connect to any NNTP server, last error: %v", lastErr)
+}
+
+func (c *NNTPClient) connectViaSocks5(proxyAddr, targetAddr string) (net.Conn, error) {
+ proxyConn, err := net.DialTimeout("tcp", proxyAddr, 30*time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to Tor proxy: %v", err)
+ }
+
+ deadline := 60 * time.Second
+ if strings.Contains(targetAddr, ".onion") {
+ deadline = 180 * time.Second
+ }
+
+ proxyConn.SetDeadline(time.Now().Add(deadline))
+ defer proxyConn.SetDeadline(time.Time{})
+
+ log.Printf("๐Ÿ”„ Performing SOCKS5 handshake for %s...", targetAddr)
+ if err := c.socks5Handshake(proxyConn, targetAddr); err != nil {
+ proxyConn.Close()
+ return nil, fmt.Errorf("SOCKS5 handshake failed: %v", err)
+ }
+
+ log.Printf("โœ… SOCKS5 connection established to %s", targetAddr)
+ return proxyConn, nil
+}
+
+func (c *NNTPClient) socks5Handshake(conn net.Conn, target string) error {
+ log.Printf("๐Ÿ”„ SOCKS5: Starting authentication...")
+ authReq := []byte{0x05, 0x01, 0x00}
+ if _, err := conn.Write(authReq); err != nil {
+ return fmt.Errorf("auth request failed: %v", err)
+ }
+
+ authResp := make([]byte, 2)
+ if _, err := io.ReadFull(conn, authResp); err != nil {
+ return fmt.Errorf("auth response read failed: %v", err)
+ }
+
+ if authResp[0] != 0x05 || authResp[1] != 0x00 {
+ return fmt.Errorf("SOCKS5 authentication failed")
+ }
+
+ log.Printf("โœ… SOCKS5: Authentication successful")
+
+ parts := strings.Split(target, ":")
+ host := parts[0]
+ port, _ := strconv.Atoi(parts[1])
+
+ log.Printf("๐Ÿ”„ SOCKS5: Connecting to %s (hostname length: %d)", target, len(host))
+ req := []byte{0x05, 0x01, 0x00, 0x03}
+ req = append(req, byte(len(host)))
+ req = append(req, []byte(host)...)
+ req = append(req, byte(port>>8), byte(port&0xff))
+
+ if _, err := conn.Write(req); err != nil {
+ return fmt.Errorf("connect request failed: %v", err)
+ }
+
+ log.Printf("๐Ÿ”„ SOCKS5: Waiting for connect response...")
+ resp := make([]byte, 4)
+ if _, err := io.ReadFull(conn, resp); err != nil {
+ return fmt.Errorf("connect response read failed: %v", err)
+ }
+
+ if resp[0] != 0x05 {
+ return fmt.Errorf("SOCKS5 version mismatch")
+ }
+
+ if resp[1] != 0x00 {
+ errorMsg := map[byte]string{
+ 0x01: "general SOCKS server failure",
+ 0x02: "connection not allowed by ruleset",
+ 0x03: "network unreachable",
+ 0x04: "host unreachable",
+ 0x05: "connection refused",
+ 0x06: "TTL expired",
+ 0x07: "command not supported",
+ 0x08: "address type not supported",
+ }
+ if msg, ok := errorMsg[resp[1]]; ok {
+ return fmt.Errorf("SOCKS5 connect failed: %s (code %d)", msg, resp[1])
+ }
+ return fmt.Errorf("SOCKS5 connect failed: error code %d", resp[1])
+ }
+
+ switch resp[3] {
+ case 0x01:
+ addr := make([]byte, 6)
+ io.ReadFull(conn, addr)
+ case 0x03:
+ domainLen := make([]byte, 1)
+ io.ReadFull(conn, domainLen)
+ domain := make([]byte, domainLen[0]+2)
+ io.ReadFull(conn, domain)
+ case 0x04:
+ addr := make([]byte, 18)
+ io.ReadFull(conn, addr)
+ }
+
+ return nil
+}
+
+func (c *NNTPClient) ensureConnected() error {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ if !c.isConnected || c.conn == nil || time.Since(c.lastUsed) > 5*time.Minute {
+ log.Printf("๐Ÿ”„ Connection check: reconnecting (connected=%v, lastUsed=%v ago)",
+ c.isConnected, time.Since(c.lastUsed))
+
+ if c.conn != nil {
+ c.conn.Close()
+ }
+ c.isConnected = false
+ c.connectedServer = ""
+ c.isOnionService = false
+
+ if err := c.testOnionReachability(); err != nil {
+ return fmt.Errorf("onion service unreachable: %v", err)
+ }
+
+ if err := c.connectInternal(); err != nil {
+ return fmt.Errorf("failed to reconnect: %v", err)
+ }
+ }
+
+ c.lastUsed = time.Now()
+ return nil
+}
+
+func (c *NNTPClient) testOnionReachability() error {
+ log.Printf("๐Ÿงช Testing NNTP server reachability...")
+
+ proxyAddr := "127.0.0.1:9050"
+ testConfigs := []struct {
+ addr string
+ isOnion bool
+ description string
+ }{
+ {
+ addr: "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119",
+ isOnion: true,
+ description: "Primary NNTP Onion Service",
+ },
+ {
+ addr: "news.tcpreset.net:119",
+ isOnion: false,
+ description: "TCPReset NNTP Server",
+ },
+ }
+
+ for _, config := range testConfigs {
+ log.Printf("๐Ÿงช Testing %s (%s)...", config.description, config.addr)
+
+ proxyConn, err := net.DialTimeout("tcp", proxyAddr, 10*time.Second)
+ if err != nil {
+ log.Printf("โš ๏ธ Tor proxy unreachable: %v", err)
+ continue
+ }
+ defer proxyConn.Close()
+
+ deadline := 30 * time.Second
+ if config.isOnion {
+ deadline = 60 * time.Second
+ }
+ proxyConn.SetDeadline(time.Now().Add(deadline))
+
+ if err := c.quickSocks5Test(proxyConn, config.addr); err != nil {
+ log.Printf("โš ๏ธ %s failed: %v", config.description, err)
+ continue
+ }
+
+ log.Printf("โœ… %s appears reachable", config.description)
+ return nil
+ }
+
+ return fmt.Errorf("no NNTP servers are reachable (tried onion service and public servers)")
+}
+
+func (c *NNTPClient) quickSocks5Test(conn net.Conn, target string) error {
+ authReq := []byte{0x05, 0x01, 0x00}
+ if _, err := conn.Write(authReq); err != nil {
+ return fmt.Errorf("auth request failed: %v", err)
+ }
+
+ authResp := make([]byte, 2)
+ if _, err := io.ReadFull(conn, authResp); err != nil {
+ return fmt.Errorf("auth response read failed: %v", err)
+ }
+
+ if authResp[0] != 0x05 || authResp[1] != 0x00 {
+ return fmt.Errorf("SOCKS5 authentication failed")
+ }
+
+ parts := strings.Split(target, ":")
+ host := parts[0]
+ port, _ := strconv.Atoi(parts[1])
+
+ req := []byte{0x05, 0x01, 0x00, 0x03}
+ req = append(req, byte(len(host)))
+ req = append(req, []byte(host)...)
+ req = append(req, byte(port>>8), byte(port&0xff))
+
+ if _, err := conn.Write(req); err != nil {
+ return fmt.Errorf("connect request failed: %v", err)
+ }
+
+ resp := make([]byte, 4)
+ if _, err := io.ReadFull(conn, resp); err != nil {
+ return fmt.Errorf("connect response read failed: %v", err)
+ }
+
+ if resp[0] != 0x05 {
+ return fmt.Errorf("SOCKS5 version mismatch")
+ }
+
+ if resp[1] != 0x00 {
+ errorMsg := map[byte]string{
+ 0x01: "general SOCKS server failure",
+ 0x02: "connection not allowed by ruleset",
+ 0x03: "network unreachable",
+ 0x04: "host unreachable",
+ 0x05: "connection refused",
+ 0x06: "TTL expired",
+ 0x07: "command not supported",
+ 0x08: "address type not supported",
+ }
+ if msg, ok := errorMsg[resp[1]]; ok {
+ return fmt.Errorf("SOCKS5 connect failed: %s", msg)
+ }
+ return fmt.Errorf("SOCKS5 connect failed: error code %d", resp[1])
+ }
+
+ switch resp[3] {
+ case 0x01:
+ addr := make([]byte, 6)
+ io.ReadFull(conn, addr)
+ case 0x03:
+ domainLen := make([]byte, 1)
+ io.ReadFull(conn, domainLen)
+ domain := make([]byte, domainLen[0]+2)
+ io.ReadFull(conn, domain)
+ case 0x04:
+ addr := make([]byte, 18)
+ io.ReadFull(conn, addr)
+ }
+
+ return nil
+}
+
+func (c *NNTPClient) modeReaderInternal() error {
+ if err := c.sendCommand("MODE READER"); err != nil {
+ return fmt.Errorf("failed to send MODE READER: %v", err)
+ }
+
+ response, err := c.readResponse()
+ if err != nil {
+ return fmt.Errorf("failed to read MODE READER response: %v", err)
+ }
+
+ if !strings.HasPrefix(response, "200") && !strings.HasPrefix(response, "201") {
+ return fmt.Errorf("MODE READER failed: %s", response)
+ }
+
+ log.Printf("โœ… MODE READER successful: %s", response)
+ return nil
+}
+
+func (c *NNTPClient) sendCommand(command string) error {
+ if c.conn == nil {
+ return fmt.Errorf("not connected")
+ }
+
+ log.Printf("๐Ÿ“ค Sending: %s", command)
+ _, err := c.conn.Write([]byte(command + "\r\n"))
+ return err
+}
+
+func (c *NNTPClient) readResponse() (string, error) {
+ if c.reader == nil {
+ return "", fmt.Errorf("not connected")
+ }
+
+ response, err := c.reader.ReadString('\n')
+ if err != nil {
+ return "", err
+ }
+
+ response = strings.TrimSpace(response)
+ log.Printf("๐Ÿ“ฅ Received: %s", response)
+ return response, nil
+}
+
+func (c *NNTPClient) HealthCheck() error {
+ log.Printf("๐Ÿงช Performing NNTP health check...")
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ if !c.isConnected || c.conn == nil {
+ return fmt.Errorf("connection is nil or not connected")
+ }
+
+ c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
+ c.conn.SetReadDeadline(time.Now().Add(10 * time.Second))
+ defer c.conn.SetWriteDeadline(time.Time{})
+ defer c.conn.SetReadDeadline(time.Time{})
+
+ // Try DATE command first (single line response, safer)
+ log.Printf("๐Ÿ“ค Health check: sending DATE command")
+ if err := c.sendCommand("DATE"); err != nil {
+ c.isConnected = false
+ return fmt.Errorf("health check send failed: %v", err)
+ }
+
+ response, err := c.readResponse()
+ if err != nil {
+ c.isConnected = false
+ return fmt.Errorf("health check read failed: %v", err)
+ }
+
+ log.Printf("๐Ÿ“ฅ Health check: received: %s", response)
+
+ // DATE should return "111 YYYYMMDDhhmmss" - single line response
+ if strings.HasPrefix(response, "111") {
+ log.Printf("โœ… Health check successful")
+ return nil
+ }
+
+ // If DATE failed, try HELP but consume multiline response properly
+ log.Printf("๐Ÿ“ค Health check: DATE failed, trying HELP command")
+ if err := c.sendCommand("HELP"); err != nil {
+ c.isConnected = false
+ return fmt.Errorf("health check HELP send failed: %v", err)
+ }
+
+ helpResponse, err := c.readResponse()
+ if err != nil {
+ c.isConnected = false
+ return fmt.Errorf("health check HELP read failed: %v", err)
+ }
+
+ log.Printf("๐Ÿ“ฅ Health check: HELP received: %s", helpResponse)
+
+ // HELP command returns multiline response - consume all lines until "."
+ if strings.HasPrefix(helpResponse, "100") {
+ log.Printf("๐Ÿ“ฅ Health check: reading multiline HELP response")
+ for {
+ line, err := c.reader.ReadString('\n')
+ if err != nil {
+ c.isConnected = false
+ return fmt.Errorf("health check multiline read failed: %v", err)
+ }
+
+ line = strings.TrimSpace(line)
+ log.Printf("๐Ÿ“ฅ Health check help: %s", line)
+
+ if line == "." {
+ break
+ }
+ }
+ log.Printf("โœ… Health check successful")
+ return nil
+ }
+
+ c.isConnected = false
+ return fmt.Errorf("health check failed: unexpected response: %s", helpResponse)
+}
+
+func (c *NNTPClient) ListGroups() (map[string]*NewsgroupInfo, error) {
+ log.Printf("Downloading active file with retry logic...")
+
+ if err := c.ensureConnected(); err != nil {
+ return nil, err
+ }
+
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ log.Printf("๐Ÿ“ค Requesting group list...")
+ if err := c.sendCommand("LIST"); err != nil {
+ return nil, fmt.Errorf("failed to send LIST command: %v", err)
+ }
+
+ response, err := c.readResponse()
+ if err != nil {
+ return nil, fmt.Errorf("failed to read LIST response: %v", err)
+ }
+
+ if !strings.HasPrefix(response, "215") {
+ return nil, fmt.Errorf("LIST command failed: %s", response)
+ }
+
+ log.Printf("๐Ÿ“ฅ Reading group data...")
+ groups := make(map[string]*NewsgroupInfo)
+ lineCount := 0
+
+ for {
+ line, err := c.reader.ReadString('\n')
+ if err != nil {
+ return nil, fmt.Errorf("error reading group data: %v", err)
+ }
+
+ line = strings.TrimSpace(line)
+ if line == "." {
+ break
+ }
+
+ lineCount++
+ if lineCount%1000 == 0 {
+ log.Printf("๐Ÿ“Š Processed %d groups...", lineCount)
+ }
+
+ parts := strings.Fields(line)
+ if len(parts) >= 4 {
+ high, _ := strconv.Atoi(parts[1])
+ low, _ := strconv.Atoi(parts[2])
+
+ groups[parts[0]] = &NewsgroupInfo{
+ Name: parts[0],
+ High: high,
+ Low: low,
+ Status: parts[3],
+ }
+ }
+ }
+
+ log.Printf("โœ… Successfully loaded %d newsgroups", len(groups))
+ return groups, nil
+}
+
+func (c *NNTPClient) SelectGroup(groupName string) (int, int, int, error) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ if !c.isConnected || c.conn == nil {
+ return 0, 0, 0, fmt.Errorf("not connected")
+ }
+
+ log.Printf("๐Ÿ“ค Selecting group: %s", groupName)
+ if err := c.sendCommand("GROUP " + groupName); err != nil {
+ return 0, 0, 0, fmt.Errorf("failed to send GROUP command: %v", err)
+ }
+
+ response, err := c.readResponse()
+ if err != nil {
+ return 0, 0, 0, fmt.Errorf("failed to read GROUP response: %v", err)
+ }
+
+ if !strings.HasPrefix(response, "211") {
+ return 0, 0, 0, fmt.Errorf("GROUP command failed: %s", response)
+ }
+
+ // Parse response: 211 count low high group
+ parts := strings.Fields(response)
+ if len(parts) < 5 {
+ return 0, 0, 0, fmt.Errorf("invalid GROUP response format: %s", response)
+ }
+
+ count, _ := strconv.Atoi(parts[1])
+ low, _ := strconv.Atoi(parts[2])
+ high, _ := strconv.Atoi(parts[3])
+
+ log.Printf("โœ… Group selected: %s (count=%d, range=%d-%d)", groupName, count, low, high)
+ return count, low, high, nil
+}
+
+func (c *NNTPClient) GetArticleOverview(low, high int, maxArticles int) ([]*ArticleOverview, error) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ if !c.isConnected || c.conn == nil {
+ return nil, fmt.Errorf("not connected")
+ }
+
+ // Limit range to prevent huge downloads
+ if high-low+1 > maxArticles {
+ low = high - maxArticles + 1
+ }
+
+ log.Printf("๐Ÿ“ค Getting article overview for range %d-%d", low, high)
+ if err := c.sendCommand(fmt.Sprintf("XOVER %d-%d", low, high)); err != nil {
+ return nil, fmt.Errorf("failed to send XOVER command: %v", err)
+ }
+
+ response, err := c.readResponse()
+ if err != nil {
+ return nil, fmt.Errorf("failed to read XOVER response: %v", err)
+ }
+
+ if !strings.HasPrefix(response, "224") {
+ return nil, fmt.Errorf("XOVER command failed: %s", response)
+ }
+
+ var articles []*ArticleOverview
+ log.Printf("๐Ÿ“ฅ Reading article overview data...")
+
+ for {
+ line, err := c.reader.ReadString('\n')
+ if err != nil {
+ return nil, fmt.Errorf("error reading overview data: %v", err)
+ }
+
+ line = strings.TrimSpace(line)
+ if line == "." {
+ break
+ }
+
+ article := c.parseOverviewLine(line)
+ if article != nil {
+ articles = append(articles, article)
+ }
+ }
+
+ log.Printf("โœ… Retrieved %d article overviews", len(articles))
+ return articles, nil
+}
+
+func (c *NNTPClient) parseOverviewLine(line string) *ArticleOverview {
+ // XOVER format: number<tab>subject<tab>from<tab>date<tab>message-id<tab>references<tab>byte count<tab>line count
+ parts := strings.Split(line, "\t")
+ if len(parts) < 8 {
+ return nil
+ }
+
+ number, _ := strconv.Atoi(parts[0])
+ byteCount, _ := strconv.Atoi(parts[6])
+ lineCount, _ := strconv.Atoi(parts[7])
+
+ return &ArticleOverview{
+ Number: number,
+ Subject: parts[1],
+ From: parts[2],
+ Date: parts[3],
+ MessageID: parts[4],
+ References: parts[5],
+ ByteCount: byteCount,
+ LineCount: lineCount,
+ }
+}
+
+func (c *NNTPClient) GetArticle(groupName string, articleNumber int) (map[string]string, []string, error) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ if !c.isConnected || c.conn == nil {
+ return nil, nil, fmt.Errorf("not connected")
+ }
+
+ log.Printf("๐Ÿ“ค Getting article %d from group %s", articleNumber, groupName)
+ if err := c.sendCommand(fmt.Sprintf("ARTICLE %d", articleNumber)); err != nil {
+ return nil, nil, fmt.Errorf("failed to send ARTICLE command: %v", err)
+ }
+
+ response, err := c.readResponse()
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read ARTICLE response: %v", err)
+ }
+
+ if !strings.HasPrefix(response, "220") {
+ return nil, nil, fmt.Errorf("ARTICLE command failed: %s", response)
+ }
+
+ // Read article content
+ headers := make(map[string]string)
+ var body []string
+ inHeaders := true
+
+ log.Printf("๐Ÿ“ฅ Reading article content...")
+ for {
+ line, err := c.reader.ReadString('\n')
+ if err != nil {
+ return nil, nil, fmt.Errorf("error reading article data: %v", err)
+ }
+
+ line = strings.TrimSuffix(line, "\r\n")
+ line = strings.TrimSuffix(line, "\n")
+
+ if line == "." {
+ break
+ }
+
+ if inHeaders {
+ if line == "" {
+ inHeaders = false
+ continue
+ }
+
+ // Parse header line
+ if strings.Contains(line, ":") {
+ parts := strings.SplitN(line, ":", 2)
+ if len(parts) == 2 {
+ key := strings.TrimSpace(parts[0])
+ value := strings.TrimSpace(parts[1])
+
+ // Handle multi-line headers
+ if existing, exists := headers[key]; exists {
+ headers[key] = existing + " " + value
+ } else {
+ headers[key] = value
+ }
+ }
+ }
+ } else {
+ body = append(body, line)
+ }
+ }
+
+ log.Printf("โœ… Retrieved article %d: %d headers, %d body lines", articleNumber, len(headers), len(body))
+ return headers, body, nil
+}
+
+func (c *NNTPClient) GetConnectionInfo() (string, bool) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+ return c.connectedServer, c.isOnionService
+}
+
+func (c *NNTPClient) Close() error {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ if c.conn != nil {
+ log.Printf("๐Ÿ“ค Sending: QUIT")
+ c.sendCommand("QUIT")
+ c.conn.Close()
+ c.conn = nil
+ c.reader = nil
+ }
+ c.isConnected = false
+ c.connectedServer = ""
+ c.isOnionService = false
+ return nil
+}
+
+func (c *NNTPClient) disconnect() {
+ if c.conn != nil {
+ c.conn.Close()
+ }
+ c.conn = nil
+ c.reader = nil
+ c.isConnected = false
+ c.connectedServer = ""
+ c.isOnionService = false
+}
+
+func (s *NewsServer) GetHealthStatus() map[string]interface{} {
+ s.mutex.RLock()
+ defer s.mutex.RUnlock()
+
+ status := map[string]interface{}{
+ "session": s.session,
+ "timestamp": time.Now().UTC().Format("2006-01-02 15:04:05 UTC"),
+ "features": s.features,
+ "target": "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion",
+ "fallback": "news.tcpreset.net",
+ "cached_groups": len(s.groups),
+ }
+
+ if connectedServer, isOnion := s.client.GetConnectionInfo(); connectedServer != "" {
+ status["connected_server"] = connectedServer
+ status["is_onion_service"] = isOnion
+ }
+
+ if err := s.testTorProxy(); err != nil {
+ status["overall"] = "degraded"
+ status["tor_proxy"] = "failed"
+ status["tor_error"] = err.Error()
+ status["nntp_status"] = "untested"
+ status["message"] = "Tor proxy is not reachable - check if Tor is running"
+ return status
+ }
+
+ status["tor_proxy"] = "ok"
+
+ if err := s.client.HealthCheck(); err != nil {
+ status["overall"] = "partial"
+ status["nntp_status"] = "disconnected"
+ status["nntp_error"] = err.Error()
+ status["http_status"] = "ok"
+
+ if strings.Contains(err.Error(), "TTL expired") {
+ status["message"] = "Onion service appears to be down or unreachable"
+ } else if strings.Contains(err.Error(), "connection reset") {
+ status["message"] = "Connection to onion service was reset"
+ } else if strings.Contains(err.Error(), "timeout") {
+ status["message"] = "Connection to onion service timed out"
+ } else {
+ status["message"] = "HTTP server running, NNTP connection will retry automatically"
+ }
+ } else {
+ status["overall"] = "healthy"
+ status["nntp_status"] = "connected"
+ status["http_status"] = "ok"
+ status["message"] = "All systems operational"
+ }
+
+ return status
+}
+
+func (s *NewsServer) testTorProxy() error {
+ log.Printf("๐Ÿงช Testing Tor proxy connectivity...")
+
+ conn, err := net.DialTimeout("tcp", "127.0.0.1:9050", 10*time.Second)
+ if err != nil {
+ return fmt.Errorf("Tor proxy not reachable: %v", err)
+ }
+ defer conn.Close()
+
+ conn.SetDeadline(time.Now().Add(10 * time.Second))
+
+ _, err = conn.Write([]byte{0x05, 0x01, 0x00})
+ if err != nil {
+ return fmt.Errorf("SOCKS5 handshake failed: %v", err)
+ }
+
+ resp := make([]byte, 2)
+ _, err = io.ReadFull(conn, resp)
+ if err != nil {
+ return fmt.Errorf("SOCKS5 response failed: %v", err)
+ }
+
+ if resp[0] != 0x05 || resp[1] != 0x00 {
+ return fmt.Errorf("SOCKS5 authentication failed: got %d,%d", resp[0], resp[1])
+ }
+
+ log.Printf("โœ… Tor proxy is reachable and responding")
+ return nil
+}
+
+func (s *NewsServer) downloadActiveFile() error {
+ s.downloadStatus.mutex.Lock()
+ s.downloadStatus.IsDownloading = true
+ s.downloadStatus.StartTime = time.Now()
+ s.downloadStatus.LastError = ""
+ s.downloadStatus.Completed = false
+ s.downloadStatus.mutex.Unlock()
+
+ log.Printf("๐Ÿ“ฅ Starting active file download with timeout...")
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ defer cancel()
+
+ resultChan := make(chan error, 1)
+
+ go func() {
+ groups, err := s.client.ListGroups()
+ if err != nil {
+ resultChan <- err
+ return
+ }
+
+ s.mutex.Lock()
+ s.groups = groups
+ s.mutex.Unlock()
+
+ s.downloadStatus.mutex.Lock()
+ s.downloadStatus.GroupCount = len(groups)
+ s.downloadStatus.mutex.Unlock()
+
+ resultChan <- nil
+ }()
+
+ select {
+ case err := <-resultChan:
+ s.downloadStatus.mutex.Lock()
+ s.downloadStatus.IsDownloading = false
+ s.downloadStatus.mutex.Unlock()
+
+ if err != nil {
+ log.Printf("โŒ Active file download failed: %v", err)
+ s.downloadStatus.mutex.Lock()
+ s.downloadStatus.LastError = err.Error()
+ s.downloadStatus.mutex.Unlock()
+ return err
+ }
+ log.Printf("โœ… Active file download completed successfully")
+ s.downloadStatus.mutex.Lock()
+ s.downloadStatus.Completed = true
+ s.downloadStatus.mutex.Unlock()
+ return nil
+ case <-ctx.Done():
+ log.Printf("โฐ Active file download timed out after 5 minutes")
+ s.downloadStatus.mutex.Lock()
+ s.downloadStatus.IsDownloading = false
+ s.downloadStatus.LastError = "Download timed out after 5 minutes"
+ s.downloadStatus.mutex.Unlock()
+ return fmt.Errorf("download timed out - onion service connection taking too long")
+ }
+}
+
+func (s *NewsServer) handleDownloadStatus(w http.ResponseWriter, r *http.Request) {
+ s.downloadStatus.mutex.RLock()
+ isDownloading := s.downloadStatus.IsDownloading
+ startTime := s.downloadStatus.StartTime
+ lastError := s.downloadStatus.LastError
+ completed := s.downloadStatus.Completed
+ groupCount := s.downloadStatus.GroupCount
+ s.downloadStatus.mutex.RUnlock()
+
+ w.Header().Set("Content-Type", "application/json")
+
+ status := map[string]interface{}{
+ "is_downloading": isDownloading,
+ "completed": completed,
+ "group_count": groupCount,
+ "timestamp": time.Now().Unix(),
+ }
+
+ if !startTime.IsZero() {
+ status["start_time"] = startTime.Unix()
+ status["elapsed_seconds"] = int(time.Since(startTime).Seconds())
+ }
+
+ if lastError != "" {
+ status["last_error"] = lastError
+ }
+
+ if isDownloading {
+ status["message"] = "Download in progress..."
+ } else if completed {
+ status["message"] = "Download completed successfully"
+ } else if lastError != "" {
+ status["message"] = "Download failed"
+ } else {
+ status["message"] = "No download in progress"
+ }
+
+ w.Write([]byte(fmt.Sprintf(`{
+ "is_downloading": %t,
+ "completed": %t,
+ "group_count": %d,
+ "message": "%s",
+ "timestamp": %d
+ %s
+ }`,
+ isDownloading,
+ completed,
+ groupCount,
+ status["message"],
+ status["timestamp"],
+ func() string {
+ extras := ""
+ if !startTime.IsZero() {
+ extras += fmt.Sprintf(`,
+ "elapsed_seconds": %d`, int(time.Since(startTime).Seconds()))
+ }
+ if lastError != "" {
+ extras += fmt.Sprintf(`,
+ "last_error": "%s"`, lastError)
+ }
+ return extras
+ }(),
+ )))
+}
+
+func (s *NewsServer) handleActive(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Query().Get("refresh") == "1" || r.Method == "POST" {
+ log.Printf("๐Ÿ“ฅ Active file download requested...")
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ processingHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>โณ Downloading... - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #ffaa00; padding: 20px; }
+ .container { max-width: 800px; margin: 0 auto; text-align: center; }
+ .spinner { animation: spin 1s linear infinite; display: inline-block; font-size: 2em; }
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
+ .processing-box { background: #1a1a1a; padding: 30px; border-radius: 8px; border: 2px solid #ffaa00; margin: 20px 0; }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .status-update { margin: 15px 0; padding: 10px; background: #2a1a00; border-radius: 4px; }
+ .success { color: #00ff00; }
+ .error { color: #ff8800; }
+ .progress { margin: 10px 0; font-size: 0.9em; color: #888; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="processing-box">
+ <h1><span class="spinner" id="spinner">๐Ÿ”„</span> <span id="status-title">Downloading Active File</span></h1>
+ <p id="status-text"><strong>Connecting to NNTP server via Tor...</strong></p>
+ <div class="status-update" id="status-details">
+ <p>โฐ <strong>This may take 2-5 minutes</strong> due to onion service connection time</p>
+ <p id="progress-text">๐Ÿ” Checking status...</p>
+ </div>
+ <p>
+ <a href="/active" class="back-link">๐Ÿ”„ Check Progress</a> |
+ <a href="/" class="back-link">โ† Back to Home</a> |
+ <a href="/health" class="back-link">๐Ÿ“Š Health Status</a>
+ </p>
+ <p class="progress"><em>Started at ` + time.Now().Format("15:04:05") + `</em></p>
+ </div>
+ </div>
+ <script>
+ let checkCount = 0;
+
+ function updateStatus() {
+ checkCount++;
+ fetch('/download-status')
+ .then(response => response.json())
+ .then(data => {
+ const spinner = document.getElementById('spinner');
+ const statusTitle = document.getElementById('status-title');
+ const statusText = document.getElementById('status-text');
+ const progressText = document.getElementById('progress-text');
+
+ if (data.completed) {
+ spinner.innerHTML = 'โœ…';
+ spinner.className = 'success';
+ statusTitle.innerHTML = 'Download Completed!';
+ statusText.innerHTML = '<strong>Successfully downloaded ' + data.group_count + ' newsgroups</strong>';
+ progressText.innerHTML = '๐ŸŽ‰ Redirecting to active groups page...';
+ setTimeout(() => { window.location.href = '/active'; }, 3000);
+ } else if (data.last_error) {
+ spinner.innerHTML = 'โŒ';
+ spinner.className = 'error';
+ statusTitle.innerHTML = 'Download Failed';
+ statusText.innerHTML = '<strong>Error: ' + data.last_error + '</strong>';
+ progressText.innerHTML = '๐Ÿ”„ <a href="/active?refresh=1" style="color: #00aaff;">Try Again</a>';
+ } else if (data.is_downloading) {
+ statusText.innerHTML = '<strong>Download in progress...</strong>';
+ if (data.elapsed_seconds) {
+ progressText.innerHTML = 'โฐ Running for ' + data.elapsed_seconds + ' seconds...';
+ }
+ } else {
+ progressText.innerHTML = 'โฐ Waiting for download to start... (check ' + checkCount + ')';
+ }
+ })
+ .catch(error => {
+ console.log('Status check failed:', error);
+ document.getElementById('progress-text').innerHTML = 'โš ๏ธ Status check failed, will retry...';
+ });
+ }
+
+ updateStatus();
+ const interval = setInterval(updateStatus, 3000);
+
+ setTimeout(() => {
+ clearInterval(interval);
+ document.getElementById('progress-text').innerHTML = 'โฐ Status checking stopped - please refresh page manually';
+ }, 10 * 60 * 1000);
+ </script>
+</body>
+</html>`
+ w.Write([]byte(processingHTML))
+
+ go func() {
+ log.Printf("๐Ÿ”„ Starting background active file download...")
+ err := s.downloadActiveFile()
+ if err != nil {
+ log.Printf("โŒ Background download failed: %v", err)
+ } else {
+ log.Printf("โœ… Background download completed successfully")
+ }
+ }()
+
+ return
+ }
+
+ s.mutex.RLock()
+ groupCount := len(s.groups)
+ s.mutex.RUnlock()
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ activeHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>๐Ÿ“‹ Active Groups - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
+ .container { max-width: 1000px; margin: 0 auto; }
+ .header { margin-bottom: 20px; }
+ .download-section { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
+ .download-button { background: #333; color: #00ff00; border: none; padding: 12px 20px; border-radius: 4px; cursor: pointer; }
+ .download-button:hover { background: #555; }
+ .group-list { background: #1a1a1a; padding: 20px; border-radius: 8px; }
+ .group-item { padding: 8px; margin: 3px 0; background: #0a0a0a; border-radius: 3px; }
+ .group-name { color: #00aaff; }
+ .group-stats { color: #888; font-size: 0.9em; }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
+ .success { background: #002a1a; border: 1px solid #00aa00; color: #00aa00; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="header">
+ <h1>๐Ÿ“‹ Active Newsgroups</h1>
+ <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/health" class="back-link">๐Ÿ“Š Health Status</a></p>
+ </div>
+
+ {{if eq .GroupCount 0}}
+ <div class="warning">
+ <h3>โš ๏ธ No Groups Cached</h3>
+ <p>No newsgroups are currently cached. You need to download the active file first.</p>
+ <p><em>Note: This may take 2-5 minutes due to onion service connection time.</em></p>
+ </div>
+ {{else}}
+ <div class="success">
+ <h3>โœ… Groups Loaded</h3>
+ <p>Currently cached: <strong>{{.GroupCount}}</strong> newsgroups</p>
+ <p>You can now browse groups or download a fresh list.</p>
+ </div>
+ {{end}}
+
+ <div class="download-section">
+ <h3>๐Ÿ“ก Download Active File</h3>
+ <p>Current groups in cache: <strong>{{.GroupCount}}</strong></p>
+ <form method="post" onsubmit="this.querySelector('button').innerHTML='โณ Starting...'; this.querySelector('button').disabled=true;">
+ <button type="submit" class="download-button">๐Ÿ”„ START DOWNLOAD</button>
+ </form>
+ <p><em>This will download the complete list of available newsgroups from the server.</em></p>
+ <p><strong>โฐ Please be patient:</strong> Onion service connections can take 2-5 minutes. The page will show progress.</p>
+ </div>
+
+ {{if .HasGroups}}
+ <div class="group-list">
+ <h3>๐Ÿ“Š Available Groups (showing first 100)</h3>
+ {{range .Groups}}
+ <div class="group-item">
+ <div class="group-name">
+ <a href="/group/{{.Name}}" style="color: #00aaff; text-decoration: none;">{{.Name}}</a>
+ </div>
+ <div class="group-stats">Range: {{.Low}}-{{.High}} ({{.Status}})</div>
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ </div>
+</body>
+</html>`
+
+ var displayGroups []*NewsgroupInfo
+ if groupCount > 0 {
+ s.mutex.RLock()
+ for _, group := range s.groups {
+ if len(displayGroups) >= 100 {
+ break
+ }
+ displayGroups = append(displayGroups, group)
+ }
+ s.mutex.RUnlock()
+
+ sort.Slice(displayGroups, func(i, j int) bool {
+ return displayGroups[i].Name < displayGroups[j].Name
+ })
+ }
+
+ tmpl, _ := template.New("active").Parse(activeHTML)
+ data := map[string]interface{}{
+ "GroupCount": groupCount,
+ "Groups": displayGroups,
+ "HasGroups": len(displayGroups) > 0,
+ }
+ tmpl.Execute(w, data)
+}
+
+func (s *NewsServer) handleHealth(w http.ResponseWriter, r *http.Request) {
+ status := s.GetHealthStatus()
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ healthHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>๐Ÿ“Š System Health - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 40px; }
+ .status-ok { color: #00ff00; }
+ .status-warn { color: #ffaa00; }
+ .status-error { color: #ff0000; }
+ .status-degraded { color: #ffaa00; }
+ .status-partial { color: #ffaa00; }
+ .status-connected { color: #00ff00; }
+ .status-disconnected { color: #ff8800; }
+ .status-failed { color: #ff0000; }
+ .container { max-width: 800px; margin: 0 auto; }
+ .status-item { margin: 10px 0; padding: 10px; background: #1a1a1a; border-radius: 5px; }
+ .diagnostic-section { background: #2a2a1a; padding: 15px; border-radius: 5px; margin: 20px 0; }
+ .diagnostic-test { margin: 8px 0; padding: 8px; background: #1a1a1a; border-radius: 3px; font-size: 0.9em; }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .test-button { background: #333; color: #00ff00; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 5px; }
+ .test-button:hover { background: #555; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>๐Ÿ“Š System Health Status</h1>
+
+ <div class="status-item">
+ <strong>Overall Status:</strong>
+ <span class="status-{{.OverallClass}}">{{.Overall}}</span>
+ </div>
+
+ {{if .TorProxy}}
+ <div class="status-item">
+ <strong>Tor Proxy:</strong>
+ <span class="status-{{.TorProxyClass}}">{{.TorProxy}}</span>
+ {{if .TorError}}
+ <br><small style="color: #ff8800;">{{.TorError}}</small>
+ {{end}}
+ </div>
+ {{end}}
+
+ <div class="status-item">
+ <strong>NNTP Connection:</strong>
+ <span class="status-{{.NNTPClass}}">{{.NNTPStatus}}</span>
+ {{if .NNTPError}}
+ <br><small style="color: #ff8800;">{{.NNTPError}}</small>
+ {{end}}
+ </div>
+
+ <div class="status-item">
+ <strong>Target Server:</strong> {{.Target}}
+ <br><strong>Fallback Server:</strong> {{.Fallback}} (via Tor)
+ {{if .ConnectedServer}}
+ <br><strong>Currently Connected:</strong> {{.ConnectedServer}}
+ {{if .IsOnionService}}(๐Ÿง… Onion Service){{else}}(๐ŸŒ Via Tor){{end}}
+ {{end}}
+ </div>
+
+ <div class="status-item">
+ <strong>Cached Groups:</strong> {{.CachedGroups}}
+ </div>
+
+ <div class="status-item">
+ <strong>Timestamp:</strong> {{.Timestamp}}
+ </div>
+
+ <div class="status-item">
+ <strong>Session:</strong> {{.Session}}
+ </div>
+
+ {{if .Message}}
+ <div class="status-item">
+ <strong>Status:</strong> {{.Message}}
+ </div>
+ {{end}}
+
+ <div class="diagnostic-section">
+ <h3>๐Ÿ”ง Diagnostic Tools</h3>
+ <p>Manual tests you can run to troubleshoot connectivity:</p>
+
+ <div class="diagnostic-test">
+ <strong>1. Test Tor connectivity (v3 onion):</strong><br>
+ <code>curl --socks5 127.0.0.1:9050 http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion</code>
+ </div>
+
+ <div class="diagnostic-test">
+ <strong>2a. Test primary onion service:</strong><br>
+ <code>timeout 120 curl --socks5 127.0.0.1:9050 -v telnet://peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119</code>
+ </div>
+
+ <div class="diagnostic-test">
+ <strong>2b. Test fallback NNTP server:</strong><br>
+ <code>timeout 60 curl --socks5 127.0.0.1:9050 -v telnet://news.tcpreset.net:119</code>
+ </div>
+
+ <div class="diagnostic-test">
+ <strong>3. Check Tor service:</strong><br>
+ <code>systemctl status tor</code> or <code>ps aux | grep tor</code>
+ </div>
+
+ <div class="diagnostic-test">
+ <strong>4. Check Tor logs:</strong><br>
+ <code>journalctl -u tor -f</code> or <code>tail -f /var/log/tor/tor.log</code>
+ </div>
+
+ <p style="margin-top: 15px;"><em>If Tor proxy test fails, Tor is not running or configured correctly.</em></p>
+ <p><em>If onion service test fails, the target service may be down.</em></p>
+ <p><em>Try the <a href="/test-onion" class="back-link">Interactive Test Page</a> for detailed diagnostics.</em></p>
+ </div>
+
+ <div class="status-item">
+ <strong>Features:</strong>
+ <ul>
+ <li>โœ… Auto-reconnect enabled</li>
+ <li>โœ… MODE READER support</li>
+ <li>โœ… Health monitoring</li>
+ <li>โœ… Connection retry logic</li>
+ <li>โœ… Broken pipe recovery</li>
+ <li>โœ… Diagnostic testing</li>
+ </ul>
+ </div>
+
+ <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/active" class="back-link">Active Groups</a></p>
+ </div>
+
+ <script>
+ setTimeout(function() {
+ window.location.reload();
+ }, 30000);
+ </script>
+</body>
+</html>`
+
+ tmpl, _ := template.New("health").Parse(healthHTML)
+
+ data := map[string]interface{}{
+ "Overall": status["overall"],
+ "OverallClass": strings.ToLower(fmt.Sprintf("%v", status["overall"])),
+ "NNTPStatus": status["nntp_status"],
+ "NNTPClass": strings.ToLower(fmt.Sprintf("%v", status["nntp_status"])),
+ "Target": status["target"],
+ "Fallback": status["fallback"],
+ "CachedGroups": status["cached_groups"],
+ "Timestamp": status["timestamp"],
+ "Session": status["session"],
+ "Message": status["message"],
+ }
+
+ if connectedServer, exists := status["connected_server"]; exists {
+ data["ConnectedServer"] = connectedServer
+ }
+ if isOnionService, exists := status["is_onion_service"]; exists {
+ data["IsOnionService"] = isOnionService
+ }
+
+ if torProxy, exists := status["tor_proxy"]; exists {
+ data["TorProxy"] = torProxy
+ data["TorProxyClass"] = strings.ToLower(fmt.Sprintf("%v", torProxy))
+ }
+
+ if nntpError, exists := status["nntp_error"]; exists {
+ data["NNTPError"] = nntpError
+ }
+
+ if torError, exists := status["tor_error"]; exists {
+ data["TorError"] = torError
+ }
+
+ tmpl.Execute(w, data)
+}
+
+func (s *NewsServer) handleTestOnion(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ testHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>๐Ÿงช Onion Service Test - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
+ .container { max-width: 800px; margin: 0 auto; }
+ .test-box { background: #1a1a1a; padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid #333; }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .test-button { background: #333; color: #00ff00; border: none; padding: 12px 20px; border-radius: 4px; cursor: pointer; margin: 5px; }
+ .test-button:hover { background: #555; }
+ .result { margin: 15px 0; padding: 15px; border-radius: 5px; }
+ .result-success { background: #002a1a; border: 1px solid #00aa00; color: #00aa00; }
+ .result-error { background: #2a1a1a; border: 1px solid #aa0000; color: #aa0000; }
+ .result-info { background: #1a2a2a; border: 1px solid #0088aa; color: #0088aa; }
+ .spinner { animation: spin 1s linear infinite; display: inline-block; }
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
+ .test-progress { display: none; }
+ pre { background: #0a0a0a; padding: 10px; border-radius: 3px; overflow-x: auto; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>๐Ÿงช Onion Service Connectivity Test</h1>
+ <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/health" class="back-link">Health Status</a></p>
+
+ <div class="test-box">
+ <h3>๐Ÿ” Manual Connectivity Tests</h3>
+ <p>These tests help diagnose onion service connectivity issues:</p>
+
+ <button onclick="testTorProxy()" class="test-button">๐ŸŒ Test Tor Proxy</button>
+ <button onclick="testDuckDuckGo()" class="test-button">๐Ÿฆ† Test DuckDuckGo Onion</button>
+ <button onclick="testNNTPOnion()" class="test-button">๐Ÿ“ก Test NNTP Onion Service</button>
+
+ <div id="test-results"></div>
+ </div>
+
+ <div class="test-box">
+ <h3>๐Ÿ“Š Known Working Test Services (v3 Onion Addresses)</h3>
+ <p>These onion services should work if your Tor is configured correctly:</p>
+ <ul>
+ <li><strong>DuckDuckGo:</strong> <code>duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion</code></li>
+ <li><strong>Facebook:</strong> <code>facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion</code></li>
+ <li><strong>ProPublica:</strong> <code>p53lf57qovyuvwsc6xnrppddxpr23otqjafwze2xypdgc3gvhxoylvvid.onion</code></li>
+ </ul>
+ <p><em>โš ๏ธ These are v3 onion addresses (56 characters) - v2 addresses (16 chars) are deprecated since 2021</em></p>
+ </div>
+
+ <div class="test-box">
+ <h3>๐ŸŽฏ Target Servers</h3>
+ <p><strong>Primary target:</strong> <code>peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119</code></p>
+ <p><strong>Fallback server:</strong> <code>news.tcpreset.net:119</code> via Tor (anonymous access)</p>
+ <p><strong>Connection method:</strong> All connections routed through Tor SOCKS5 proxy</p>
+
+ <div style="margin-top: 15px; padding: 10px; background: #1a2a1a; border-radius: 5px;">
+ <strong>โ„น๏ธ Connection priority:</strong>
+ <ol style="margin: 10px 0 0 20px;">
+ <li>First tries the onion service for maximum anonymity</li>
+ <li>If onion service fails, falls back to <code>news.tcpreset.net</code> via Tor</li>
+ <li>All traffic is encrypted and routed through Tor network</li>
+ <li>Your IP address is hidden from NNTP servers</li>
+ </ol>
+ </div>
+
+ <div style="margin-top: 15px; padding: 10px; background: #2a1a00; border-radius: 5px;">
+ <strong>โš ๏ธ Common issues:</strong>
+ <ul style="margin: 10px 0 0 20px;">
+ <li>Onion service temporarily offline (TTL expired)</li>
+ <li>Network connectivity issues</li>
+ <li>Tor circuit building problems</li>
+ <li>Fallback server configuration/access issues</li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="test-box">
+ <h3>๐Ÿ“ก NNTP Protocol Information</h3>
+ <p><strong>Protocol type:</strong> NNTP Client (newsreader) - <strong>READ-ONLY</strong></p>
+ <p><strong>Commands used:</strong></p>
+ <ul style="margin: 10px 0 0 20px; font-family: monospace; font-size: 0.9em;">
+ <li><code>MODE READER</code> - Enter reader mode</li>
+ <li><code>LIST</code> - Get newsgroup list</li>
+ <li><code>GROUP &lt;name&gt;</code> - Select newsgroup</li>
+ <li><code>XOVER &lt;range&gt;</code> - Get article overview</li>
+ <li><code>ARTICLE &lt;id&gt;</code> - Retrieve article</li>
+ <li><code>DATE</code>/<code>HELP</code> - Health check</li>
+ <li><code>QUIT</code> - Close connection</li>
+ </ul>
+ <p><strong>NOT used:</strong> <code>IHAVE</code>, <code>POST</code>, <code>TAKETHIS</code> (server-to-server/posting commands)</p>
+
+ <div style="margin-top: 15px; padding: 10px; background: #1a2a1a; border-radius: 5px;">
+ <strong>โ„น๏ธ Technical details:</strong>
+ <ul style="margin: 10px 0 0 20px;">
+ <li>This is a <strong>newsreader client</strong>, not an NNTP server</li>
+ <li>Read-only access - no posting capabilities</li>
+ <li>Uses standard RFC 3977 NNTP commands</li>
+ <li>All traffic routed through SOCKS5 (Tor)</li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="test-box">
+ <h3>๐Ÿ”ง Command Line Tests</h3>
+ <p>You can also test manually from command line:</p>
+ <pre># Test Tor connectivity
+curl --socks5 127.0.0.1:9050 http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion
+
+# Test NNTP onion service
+timeout 120 curl --socks5 127.0.0.1:9050 -v \\
+ telnet://peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119
+
+# Test fallback server (anonymous access)
+timeout 60 curl --socks5 127.0.0.1:9050 -v \\
+ telnet://news.tcpreset.net:119</pre>
+ </div>
+
+ <div class="test-box">
+ <h3>๐Ÿ’ก Troubleshooting Tips</h3>
+ <ul>
+ <li>If Tor proxy test fails: Tor is not running or misconfigured</li>
+ <li>If DuckDuckGo test fails: Network/Tor circuit issues</li>
+ <li>If NNTP onion test fails: The specific NNTP service may be down</li>
+ <li>Try again in a few minutes - onion services can be slow/unreliable</li>
+ <li>Check Tor logs: <code>journalctl -u tor -f</code></li>
+ </ul>
+ </div>
+ </div>
+
+ <script>
+ function showResult(type, message) {
+ const resultsDiv = document.getElementById('test-results');
+ const resultClass = type === 'success' ? 'result-success' :
+ type === 'error' ? 'result-error' : 'result-info';
+ resultsDiv.innerHTML += '<div class="result ' + resultClass + '">' + message + '</div>';
+ }
+
+ function showProgress(message) {
+ const resultsDiv = document.getElementById('test-results');
+ resultsDiv.innerHTML += '<div class="result result-info"><span class="spinner">๐Ÿ”„</span> ' + message + '</div>';
+ }
+
+ async function testTorProxy() {
+ showProgress('Testing Tor proxy connectivity...');
+ try {
+ const response = await fetch('/health');
+ const text = await response.text();
+ if (text.includes('status-ok') || text.includes('Tor Proxy')) {
+ showResult('success', 'โœ… Tor proxy appears to be working');
+ } else {
+ showResult('error', 'โŒ Tor proxy test inconclusive');
+ }
+ } catch (error) {
+ showResult('error', 'โŒ Could not test Tor proxy: ' + error.message);
+ }
+ }
+
+ async function testDuckDuckGo() {
+ showProgress('Testing DuckDuckGo onion service (this may take 30-60 seconds)...');
+ showResult('info', '๐Ÿ’ก Manual test required - run: curl --socks5 127.0.0.1:9050 http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion');
+ showResult('info', 'โš ๏ธ Note: This is the current v3 onion address (v2 addresses are deprecated since 2021)');
+ }
+
+ async function testNNTPOnion() {
+ showProgress('Testing NNTP onion service (this may take 2-3 minutes)...');
+ try {
+ const response = await fetch('/download-status');
+ const data = await response.json();
+ showResult('info', '๐Ÿ“Š Current NNTP status: ' + data.message);
+ } catch (error) {
+ showResult('error', 'โŒ Could not check NNTP status: ' + error.message);
+ }
+
+ showResult('info', '๐Ÿ’ก For full test, try downloading the active file from the main page');
+ }
+ </script>
+</body>
+</html>`
+
+ w.Write([]byte(testHTML))
+}
+
+func (s *NewsServer) handleHome(w http.ResponseWriter, r *http.Request) {
+ s.mutex.RLock()
+ groupCount := len(s.groups)
+ s.mutex.RUnlock()
+
+ connectionStatus := "unknown"
+ torStatus := "unknown"
+
+ if err := s.testTorProxy(); err != nil {
+ torStatus = "failed"
+ connectionStatus = "tor-proxy-failed"
+ } else {
+ torStatus = "ok"
+ if err := s.client.HealthCheck(); err != nil {
+ connectionStatus = "nntp-disconnected"
+ } else {
+ connectionStatus = "connected"
+ }
+ }
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ homeHTML := `<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>๐Ÿง… Onion Newsreader</title>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body {
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ background: #0a0a0a;
+ color: #00ff00;
+ line-height: 1.6;
+ }
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
+ .header {
+ background: #1a1a1a;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ border: 1px solid #333;
+ }
+ .header h1 { color: #00ff00; margin-bottom: 10px; }
+ .header p { color: #888; }
+ .status {
+ background: #1a1a1a;
+ padding: 15px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ border: 1px solid #333;
+ }
+ .search-box {
+ background: #1a1a1a;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ border: 1px solid #333;
+ }
+ .search-box input {
+ width: 100%;
+ padding: 12px;
+ background: #0a0a0a;
+ border: 1px solid #333;
+ color: #00ff00;
+ border-radius: 4px;
+ font-family: monospace;
+ }
+ .search-box button {
+ padding: 12px 20px;
+ background: #333;
+ border: none;
+ color: #00ff00;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-top: 10px;
+ font-family: monospace;
+ }
+ .search-box button:hover { background: #555; }
+ .actions {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 15px;
+ margin-bottom: 20px;
+ }
+ .action-card {
+ background: #1a1a1a;
+ padding: 20px;
+ border-radius: 8px;
+ border: 1px solid #333;
+ text-align: center;
+ }
+ .action-card a {
+ color: #00aaff;
+ text-decoration: none;
+ font-weight: bold;
+ }
+ .action-card a:hover { color: #00ff00; }
+ .footer {
+ text-align: center;
+ color: #666;
+ margin-top: 40px;
+ padding: 20px;
+ }
+ .status-good { color: #00ff00; }
+ .status-warn { color: #ffaa00; }
+ .status-error { color: #ff0000; }
+ .status-unknown { color: #888; }
+ .alert { padding: 15px; border-radius: 8px; margin-bottom: 20px; }
+ .alert-error { background: #2a1a1a; border: 1px solid #ff8800; color: #ff8800; }
+ .alert-warning { background: #2a2a1a; border: 1px solid #ffaa00; color: #ffaa00; }
+ .alert-info { background: #1a2a2a; border: 1px solid #00aaff; color: #00aaff; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="header">
+ <h1>๐Ÿง… Onion Newsreader</h1>
+ <p>Anonymous NNTP Client via Tor โ€ข Secure Newsgroup Access</p>
+ </div>
+
+ {{if eq .ConnectionStatus "tor-proxy-failed"}}
+ <div class="alert alert-error">
+ <h3>โŒ Tor Connection Failed</h3>
+ <p>Cannot connect to Tor proxy at 127.0.0.1:9050</p>
+ <p>Please ensure Tor is installed and running: <code>systemctl start tor</code></p>
+ </div>
+ {{else if eq .ConnectionStatus "nntp-disconnected"}}
+ <div class="alert alert-warning">
+ <h3>โš ๏ธ NNTP Services Unreachable</h3>
+ <p>Tor proxy is working but neither the onion service nor fallback server are reachable.</p>
+ <p><strong>Primary target:</strong> <code>peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion</code></p>
+ <p><strong>Fallback server:</strong> <code>news.tcpreset.net</code> via Tor (anonymous access)</p>
+ <p>This may be temporary - services might be down or experiencing high load.</p>
+ <p><a href="/test-onion" style="color: #00aaff;">๐Ÿงช Run diagnostic tests</a> to troubleshoot.</p>
+ </div>
+ {{else if eq .ConnectionStatus "connected"}}
+ <div class="alert alert-info">
+ <h3>โœ… All Systems Operational</h3>
+ <p>Tor proxy and NNTP service are both reachable.</p>
+ </div>
+ {{end}}
+
+ <div class="status">
+ <h3>๐Ÿ“Š Status</h3>
+ <p>๐Ÿ“ก Primary: <code>peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion</code></p>
+ <p>๐Ÿ”„ Fallback: <code>news.tcpreset.net</code> via Tor (anonymous access)</p>
+ <p>๐ŸŒ Tor Proxy: <span class="status-{{.TorStatusClass}}">{{.TorStatus}}</span></p>
+ <p>๐Ÿ“ก NNTP: <span class="status-{{.ConnectionStatusClass}}">{{.ConnectionStatus}}</span></p>
+ <p>๐Ÿ“Š Cached Groups: <span class="status-good">{{.GroupCount}}</span></p>
+ <p>๐Ÿ”— Session: <code>{{.Session}}</code></p>
+ <p>โšก <a href="/health" style="color: #00aaff;">Detailed Health Check</a></p>
+ </div>
+
+ {{if ne .ConnectionStatus "tor-proxy-failed"}}
+ <div class="search-box">
+ <h3>๐Ÿ” Search Newsgroups</h3>
+ <form action="/search" method="get">
+ <input type="text" name="q" placeholder="Search newsgroups (e.g., comp.*, alt.*, *.linux.*)" required>
+ <button type="submit">Search Groups</button>
+ </form>
+ </div>
+
+ <div class="actions">
+ <div class="action-card">
+ <h4>๐Ÿ“‹ Browse Groups</h4>
+ <p><a href="/active">View All Groups</a></p>
+ <p>Browse available newsgroups</p>
+ </div>
+
+ <div class="action-card">
+ <h4>๐Ÿ”„ Refresh</h4>
+ <p><a href="/active?refresh=1">Refresh Group List</a></p>
+ <p>Update newsgroup cache</p>
+ </div>
+
+ <div class="action-card">
+ <h4>๐Ÿ“Š System Info</h4>
+ <p><a href="/health">Health Status</a></p>
+ <p>Connection and system status</p>
+ </div>
+
+ <div class="action-card">
+ <h4>๐Ÿงช Diagnostics</h4>
+ <p><a href="/test-onion">Test Onion Services</a></p>
+ <p>Connectivity troubleshooting</p>
+ </div>
+
+ <div class="action-card">
+ <h4>๐Ÿ” Quick Search</h4>
+ <p><a href="/search?q=comp.*">comp.*</a> | <a href="/search?q=alt.*">alt.*</a></p>
+ <p><a href="/search?q=*linux*">*linux*</a> | <a href="/search?q=sci.*">sci.*</a></p>
+ <p>Popular newsgroup categories</p>
+ </div>
+ </div>
+ {{else}}
+ <div class="alert alert-error">
+ <h3>๐Ÿ”ง Troubleshooting Steps</h3>
+ <ol>
+ <li>Install Tor: <code>apt-get install tor</code></li>
+ <li>Start Tor service: <code>systemctl start tor</code></li>
+ <li>Enable Tor service: <code>systemctl enable tor</code></li>
+ <li>Check Tor status: <code>systemctl status tor</code></li>
+ <li>Refresh this page after fixing Tor</li>
+ </ol>
+ </div>
+ {{end}}
+
+ <div class="footer">
+ <p>๐Ÿ”’ All connections are routed through Tor for maximum anonymity</p>
+ <p>๐Ÿง… Primary: Onion service โ€ข ๐Ÿ”„ Fallback: news.tcpreset.net via Tor (anonymous)</p>
+ <p>โœ๏ธ Posting URL: <code>http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880/</code></p>
+ </div>
+ </div>
+</body>
+</html>`
+
+ tmpl, _ := template.New("home").Parse(homeHTML)
+
+ torStatusClass := "unknown"
+ connectionStatusClass := "unknown"
+
+ switch torStatus {
+ case "ok":
+ torStatusClass = "good"
+ case "failed":
+ torStatusClass = "error"
+ }
+
+ switch connectionStatus {
+ case "connected":
+ connectionStatusClass = "good"
+ case "nntp-disconnected":
+ connectionStatusClass = "warn"
+ case "tor-proxy-failed":
+ connectionStatusClass = "error"
+ }
+
+ data := map[string]interface{}{
+ "GroupCount": groupCount,
+ "Session": s.session,
+ "ConnectionStatus": connectionStatus,
+ "ConnectionStatusClass": connectionStatusClass,
+ "TorStatus": torStatus,
+ "TorStatusClass": torStatusClass,
+ }
+ tmpl.Execute(w, data)
+}
+
+func (s *NewsServer) handleSearch(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ query := strings.TrimSpace(r.URL.Query().Get("q"))
+
+ if query == "" {
+ // Show search form
+ searchFormHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>๐Ÿ” Search Newsgroups - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
+ .container { max-width: 800px; margin: 0 auto; }
+ .search-box { background: #1a1a1a; padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid #333; }
+ .search-box input { width: 100%; padding: 12px; background: #0a0a0a; border: 1px solid #333; color: #00ff00; border-radius: 4px; font-family: monospace; }
+ .search-box button { padding: 12px 20px; background: #333; border: none; color: #00ff00; border-radius: 4px; cursor: pointer; margin-top: 10px; font-family: monospace; }
+ .search-box button:hover { background: #555; }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .examples { background: #1a1a1a; padding: 15px; border-radius: 8px; margin: 20px 0; border: 1px solid #333; }
+ .examples ul { margin: 10px 0 0 20px; }
+ .examples li { margin: 5px 0; }
+ .examples code { background: #0a0a0a; padding: 2px 4px; border-radius: 2px; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>๐Ÿ” Search Newsgroups</h1>
+ <p><a href="/" class="back-link">โ† Back to Home</a></p>
+
+ <div class="search-box">
+ <h3>Enter Search Query</h3>
+ <form action="/search" method="get">
+ <input type="text" name="q" placeholder="Search newsgroups (e.g., comp.*, alt.*, *.linux.*)" autofocus>
+ <button type="submit">๐Ÿ” Search</button>
+ </form>
+ </div>
+
+ <div class="examples">
+ <h3>๐Ÿ“ Search Examples</h3>
+ <ul>
+ <li><code>comp.*</code> - All computer-related groups</li>
+ <li><code>alt.*</code> - All alternative groups</li>
+ <li><code>*.linux.*</code> - All Linux-related groups</li>
+ <li><code>rec.sport.*</code> - All recreation sport groups</li>
+ <li><code>*security*</code> - All groups containing "security"</li>
+ <li><code>news.*</code> - All news groups</li>
+ <li><code>sci.*</code> - All science groups</li>
+ <li><code>soc.*</code> - All social groups</li>
+ </ul>
+ <p><strong>Wildcards:</strong> Use <code>*</code> to match any characters</p>
+ <p><strong>Case:</strong> Search is case-insensitive</p>
+ </div>
+ </div>
+</body>
+</html>`
+ w.Write([]byte(searchFormHTML))
+ return
+ }
+
+ // Perform search
+ s.mutex.RLock()
+ allGroups := s.groups
+ groupCount := len(allGroups)
+ s.mutex.RUnlock()
+
+ if groupCount == 0 {
+ noGroupsHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>๐Ÿ” Search Results - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
+ .container { max-width: 800px; margin: 0 auto; }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>๐Ÿ” Search Results</h1>
+ <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/search" class="back-link">๐Ÿ” New Search</a></p>
+
+ <div class="warning">
+ <h3>โš ๏ธ No Groups Available</h3>
+ <p>No newsgroups are currently cached. You need to download the active file first.</p>
+ <p><a href="/active?refresh=1" style="color: #00aaff;">๐Ÿ“ฅ Download Active File</a></p>
+ </div>
+ </div>
+</body>
+</html>`
+ w.Write([]byte(noGroupsHTML))
+ return
+ }
+
+ // Convert search pattern to regex
+ pattern := strings.ToLower(query)
+ pattern = strings.ReplaceAll(pattern, ".", "\\.")
+ pattern = strings.ReplaceAll(pattern, "*", ".*")
+ pattern = "^" + pattern + "$"
+
+ log.Printf("๐Ÿ” Searching for pattern: %s (regex: %s)", query, pattern)
+
+ var matches []*NewsgroupInfo
+ for _, group := range allGroups {
+ if matched, _ := regexp.MatchString(pattern, strings.ToLower(group.Name)); matched {
+ matches = append(matches, group)
+ }
+ }
+
+ // Sort results
+ sort.Slice(matches, func(i, j int) bool {
+ return matches[i].Name < matches[j].Name
+ })
+
+ // Limit results to prevent huge pages
+ maxResults := 500
+ truncated := false
+ if len(matches) > maxResults {
+ matches = matches[:maxResults]
+ truncated = true
+ }
+
+ log.Printf("๐Ÿ” Search completed: %d matches for '%s'", len(matches), query)
+
+ searchResultsHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>๐Ÿ” Search Results - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
+ .container { max-width: 1000px; margin: 0 auto; }
+ .search-header { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
+ .search-box { margin: 15px 0; }
+ .search-box input { width: 70%; padding: 8px; background: #0a0a0a; border: 1px solid #333; color: #00ff00; border-radius: 4px; font-family: monospace; }
+ .search-box button { padding: 8px 16px; background: #333; border: none; color: #00ff00; border-radius: 4px; cursor: pointer; margin-left: 10px; }
+ .search-box button:hover { background: #555; }
+ .results-summary { background: #1a1a1a; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
+ .group-list { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; }
+ .group-item { padding: 8px; margin: 3px 0; background: #0a0a0a; border-radius: 3px; }
+ .group-name { color: #00aaff; }
+ .group-stats { color: #888; font-size: 0.9em; }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ .info { background: #1a2a2a; border: 1px solid #0088aa; color: #0088aa; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="search-header">
+ <h1>๐Ÿ” Search Results</h1>
+ <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/search" class="back-link">๐Ÿ” New Search</a> | <a href="/active" class="back-link">๐Ÿ“‹ All Groups</a></p>
+
+ <div class="search-box">
+ <form action="/search" method="get">
+ <input type="text" name="q" value="{{.Query}}" placeholder="Search newsgroups...">
+ <button type="submit">๐Ÿ” Search</button>
+ </form>
+ </div>
+ </div>
+
+ <div class="results-summary">
+ <h3>๐Ÿ“Š Search Results</h3>
+ <p><strong>Query:</strong> <code>{{.Query}}</code></p>
+ <p><strong>Matches:</strong> {{.MatchCount}} out of {{.TotalGroups}} total groups</p>
+ {{if .Truncated}}
+ <p><strong>โš ๏ธ Results limited to first {{.MaxResults}} matches - try a more specific search</strong></p>
+ {{end}}
+ </div>
+
+ {{if eq .MatchCount 0}}
+ <div class="info">
+ <h3>โ„น๏ธ No Matches Found</h3>
+ <p>No newsgroups match your search pattern: <code>{{.Query}}</code></p>
+ <p><strong>Tips:</strong></p>
+ <ul>
+ <li>Try using wildcards: <code>comp.*</code>, <code>*linux*</code>, <code>alt.*</code></li>
+ <li>Check spelling and case (search is case-insensitive)</li>
+ <li>Use broader patterns: <code>*security*</code> instead of <code>comp.security.*</code></li>
+ </ul>
+ </div>
+ {{else}}
+ <div class="group-list">
+ <h3>๐Ÿ“‹ Matching Groups</h3>
+ {{range .Groups}}
+ <div class="group-item">
+ <div class="group-name">
+ <a href="/group/{{.Name}}" style="color: #00aaff; text-decoration: none;">{{.Name}}</a>
+ </div>
+ <div class="group-stats">Range: {{.Low}}-{{.High}} ({{.Status}}) โ€ข Articles: {{.ArticleCount}}</div>
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ </div>
+</body>
+</html>`
+
+ tmpl, _ := template.New("search").Parse(searchResultsHTML)
+
+ // Calculate article counts for display
+ for _, group := range matches {
+ if group.High > group.Low {
+ group.ArticleCount = group.High - group.Low + 1
+ } else {
+ group.ArticleCount = 0
+ }
+ }
+
+ data := map[string]interface{}{
+ "Query": query,
+ "Groups": matches,
+ "MatchCount": len(matches),
+ "TotalGroups": groupCount,
+ "Truncated": truncated,
+ "MaxResults": maxResults,
+ }
+ tmpl.Execute(w, data)
+}
+
+func (s *NewsServer) handleGroup(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ // Extract group name from URL path
+ path := strings.TrimPrefix(r.URL.Path, "/group/")
+ if path == "" {
+ http.Redirect(w, r, "/active", http.StatusSeeOther)
+ return
+ }
+
+ groupName := path
+ log.Printf("๐Ÿ” Viewing group: %s", groupName)
+
+ // Check if group exists in cache
+ s.mutex.RLock()
+ groupInfo, exists := s.groups[groupName]
+ s.mutex.RUnlock()
+
+ if !exists {
+ notFoundHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>โŒ Group Not Found - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
+ .container { max-width: 800px; margin: 0 auto; }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .error { background: #2a1a1a; border: 1px solid #aa0000; color: #aa0000; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>โŒ Newsgroup Not Found</h1>
+ <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/active" class="back-link">๐Ÿ“‹ All Groups</a></p>
+
+ <div class="error">
+ <h3>Group Not Available</h3>
+ <p>The newsgroup <strong>` + groupName + `</strong> was not found in the cached group list.</p>
+ <p>This could mean:</p>
+ <ul>
+ <li>The group name is misspelled</li>
+ <li>The group doesn't exist on this server</li>
+ <li>The active file cache needs to be refreshed</li>
+ </ul>
+ <p><a href="/active?refresh=1" style="color: #00aaff;">๐Ÿ”„ Refresh Group Cache</a></p>
+ </div>
+ </div>
+</body>
+</html>`
+ w.Write([]byte(notFoundHTML))
+ return
+ }
+
+ // Try to get articles for this group
+ var articles []*ArticleOverview
+ var articleCount, low, high int
+ var connectionError string
+
+ if err := s.client.ensureConnected(); err != nil {
+ connectionError = fmt.Sprintf("Connection failed: %v", err)
+ } else {
+ var err error
+ articleCount, low, high, err = s.client.SelectGroup(groupName)
+ if err != nil {
+ connectionError = fmt.Sprintf("Failed to select group: %v", err)
+ } else {
+ maxArticles := 100 // Limit to prevent huge pages
+ articles, err = s.client.GetArticleOverview(low, high, maxArticles)
+ if err != nil {
+ connectionError = fmt.Sprintf("Failed to get articles: %v", err)
+ }
+ }
+ }
+
+ groupHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>๐Ÿ“ฐ {{.GroupName}} - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
+ .container { max-width: 1200px; margin: 0 auto; }
+ .header { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
+ .group-info { background: #1a1a1a; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
+ .article-list { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; }
+ .article-item {
+ padding: 12px;
+ margin: 5px 0;
+ background: #0a0a0a;
+ border-radius: 3px;
+ border-left: 3px solid #333;
+ }
+ .article-item:hover {
+ background: #1a1a0a;
+ border-left-color: #ffaa00;
+ }
+ .article-subject {
+ color: #00aaff;
+ font-weight: bold;
+ margin-bottom: 5px;
+ }
+ .article-meta {
+ color: #888;
+ font-size: 0.9em;
+ }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .error { background: #2a1a1a; border: 1px solid #aa0000; color: #aa0000; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ .info { background: #1a2a2a; border: 1px solid #0088aa; color: #0088aa; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ .refresh-btn { background: #333; color: #00ff00; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 10px 5px; }
+ .refresh-btn:hover { background: #555; }
+ .pagination { text-align: center; margin: 20px 0; }
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
+ .stat-item { background: #0a0a0a; padding: 10px; border-radius: 4px; text-align: center; }
+ .posting-section { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; text-align: center; }
+ .post-button {
+ display: inline-block;
+ background: #2a4a2a;
+ color: #00ff00;
+ text-decoration: none;
+ padding: 12px 24px;
+ border-radius: 6px;
+ font-weight: bold;
+ margin: 10px 0;
+ border: 2px solid #00aa00;
+ transition: all 0.3s ease;
+ }
+ .post-button:hover {
+ background: #3a6a3a;
+ border-color: #00ff00;
+ box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
+ }
+ .posting-note {
+ font-size: 0.9em;
+ color: #888;
+ margin-top: 15px;
+ text-align: left;
+ background: #0a0a0a;
+ padding: 10px;
+ border-radius: 4px;
+ border-left: 3px solid #ffaa00;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="header">
+ <h1>๐Ÿ“ฐ {{.GroupName}}</h1>
+ <p><a href="/" class="back-link">โ† Back to Home</a> |
+ <a href="/active" class="back-link">๐Ÿ“‹ All Groups</a> |
+ <a href="/search?q={{.GroupPattern}}" class="back-link">๐Ÿ” Similar Groups</a></p>
+ </div>
+
+ <div class="group-info">
+ <h3>๐Ÿ“Š Group Information</h3>
+ <div class="stats">
+ <div class="stat-item">
+ <strong>๐Ÿ“ Cached Range:</strong><br>{{.CachedLow}} - {{.CachedHigh}}
+ </div>
+ <div class="stat-item">
+ <strong>๐Ÿ“Š Cached Count:</strong><br>{{.CachedCount}}
+ </div>
+ {{if .LiveStats}}
+ <div class="stat-item">
+ <strong>๐Ÿ“ก Live Range:</strong><br>{{.LiveLow}} - {{.LiveHigh}}
+ </div>
+ <div class="stat-item">
+ <strong>๐Ÿ”ข Live Count:</strong><br>{{.LiveCount}}
+ </div>
+ {{end}}
+ <div class="stat-item">
+ <strong>๐Ÿ“‹ Status:</strong><br>{{.Status}}
+ </div>
+ </div>
+ </div>
+
+ <div class="posting-section">
+ <h3>โœ๏ธ Post to Newsgroup</h3>
+ <p>Create a new post in <strong>{{.GroupName}}</strong></p>
+ <a href="http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880/?newsgroups={{.EncodedGroupName}}" target="_blank" class="post-button">
+ ๐Ÿ“ CREATE NEW POST
+ </a>
+ <p class="posting-note">
+ <strong>โš ๏ธ Posting Requirements:</strong><br>
+ โ€ข Use <strong>Tor Browser</strong> or configure SOCKS5 proxy (127.0.0.1:9050)<br>
+ โ€ข Link opens in new tab to posting service<br>
+ โ€ข Anonymous posting available on news.tcpreset.net
+ </p>
+ </div>
+
+ {{if .ConnectionError}}
+ <div class="error">
+ <h3>โŒ Connection Error</h3>
+ <p>{{.ConnectionError}}</p>
+ <p>Showing cached information only. <a href="/health" style="color: #00aaff;">Check system health</a></p>
+ </div>
+ {{end}}
+
+ {{if .Articles}}
+ <div class="article-list">
+ <h3>๐Ÿ“ฐ Recent Articles {{if .TruncatedView}}(showing last {{.MaxArticles}}){{end}}</h3>
+
+ {{range .Articles}}
+ <div class="article-item">
+ <div class="article-subject">
+ <a href="/article/{{$.GroupName}}/{{.Number}}" style="color: #00aaff; text-decoration: none;">
+ {{.Subject}}
+ </a>
+ </div>
+ <div class="article-meta">
+ <strong>From:</strong> {{.From}} |
+ <strong>Date:</strong> {{.Date}} |
+ <strong>Article:</strong> {{.Number}} |
+ <strong>Size:</strong> {{.ByteCount}} bytes ({{.LineCount}} lines)
+ </div>
+ </div>
+ {{end}}
+
+ {{if .TruncatedView}}
+ <div class="info">
+ <p><strong>โ„น๏ธ Note:</strong> Only showing the most recent {{.MaxArticles}} articles to prevent slow loading.</p>
+ <p>Use a dedicated newsreader client for full group browsing and threading.</p>
+ </div>
+ {{end}}
+ </div>
+ {{else if not .ConnectionError}}
+ <div class="warning">
+ <h3>โš ๏ธ No Articles Available</h3>
+ <p>This group appears to be empty or articles couldn't be retrieved.</p>
+ <p>This could be normal for:</p>
+ <ul>
+ <li>Newly created groups</li>
+ <li>Low-traffic groups</li>
+ <li>Groups with expired articles</li>
+ </ul>
+ <button onclick="window.location.reload()" class="refresh-btn">๐Ÿ”„ Refresh</button>
+ </div>
+ {{end}}
+
+ <div class="info">
+ <h3>๐Ÿ’ก Navigation Tips</h3>
+ <ul>
+ <li><strong>Click article subjects</strong> to read full articles</li>
+ <li><strong>Use browser back</strong> to return to this group view</li>
+ <li><strong>Search for similar groups</strong> using the pattern above</li>
+ <li><strong>Recent articles shown first</strong> (newest at top)</li>
+ </ul>
+ </div>
+ </div>
+</body>
+</html>`
+
+ tmpl, _ := template.New("group").Parse(groupHTML)
+
+ // Reverse articles to show newest first
+ for i := len(articles)/2 - 1; i >= 0; i-- {
+ opp := len(articles) - 1 - i
+ articles[i], articles[opp] = articles[opp], articles[i]
+ }
+
+ // Generate search pattern for similar groups
+ groupPattern := ""
+ if strings.Contains(groupName, ".") {
+ parts := strings.Split(groupName, ".")
+ if len(parts) > 1 {
+ groupPattern = parts[0] + ".*"
+ }
+ }
+
+ data := map[string]interface{}{
+ "GroupName": groupName,
+ "GroupPattern": groupPattern,
+ "CachedLow": groupInfo.Low,
+ "CachedHigh": groupInfo.High,
+ "CachedCount": groupInfo.High - groupInfo.Low + 1,
+ "Status": groupInfo.Status,
+ "Articles": articles,
+ "ConnectionError": connectionError,
+ "LiveStats": connectionError == "",
+ "MaxArticles": 100,
+ "TruncatedView": len(articles) >= 100,
+ "EncodedGroupName": url.QueryEscape(groupName),
+ }
+
+ if connectionError == "" {
+ data["LiveLow"] = low
+ data["LiveHigh"] = high
+ data["LiveCount"] = articleCount
+ }
+
+ tmpl.Execute(w, data)
+}
+
+func (s *NewsServer) handleArticle(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ // Extract group name and article number from URL path
+ path := strings.TrimPrefix(r.URL.Path, "/article/")
+ parts := strings.Split(path, "/")
+
+ if len(parts) != 2 {
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+ return
+ }
+
+ groupName := parts[0]
+ articleNumber, err := strconv.Atoi(parts[1])
+ if err != nil {
+ http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
+ return
+ }
+
+ log.Printf("๐Ÿ“ฐ Reading article %d from group %s", articleNumber, groupName)
+
+ // Ensure we're connected and select the group first
+ var headers map[string]string
+ var body []string
+ var connectionError string
+
+ if err := s.client.ensureConnected(); err != nil {
+ connectionError = fmt.Sprintf("Connection failed: %v", err)
+ } else {
+ // Select group first
+ _, _, _, err := s.client.SelectGroup(groupName)
+ if err != nil {
+ connectionError = fmt.Sprintf("Failed to select group: %v", err)
+ } else {
+ // Get the article
+ headers, body, err = s.client.GetArticle(groupName, articleNumber)
+ if err != nil {
+ connectionError = fmt.Sprintf("Failed to get article: %v", err)
+ }
+ }
+ }
+
+ articleHTML := `<!DOCTYPE html>
+<html>
+<head>
+ <title>๐Ÿ“ฐ Article {{.ArticleNumber}} - {{.GroupName}} - Onion Newsreader</title>
+ <meta charset="utf-8">
+ <style>
+ body { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; background: #0a0a0a; color: #00ff00; padding: 20px; line-height: 1.4; }
+ .container { max-width: 1000px; margin: 0 auto; }
+ .header { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
+ .article-headers { background: #1a1a1a; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
+ .article-body { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; }
+ .header-item { margin: 5px 0; padding: 5px; background: #0a0a0a; border-radius: 3px; }
+ .header-label { color: #ffaa00; font-weight: bold; }
+ .header-value { color: #00aaff; word-break: break-word; }
+ .body-content {
+ color: #cccccc;
+ white-space: pre-wrap;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ background: #0a0a0a;
+ padding: 15px;
+ border-radius: 5px;
+ border: 1px solid #333;
+ overflow-x: auto;
+ line-height: 1.4;
+ word-wrap: break-word;
+ }
+ .back-link { color: #00aaff; text-decoration: none; }
+ .back-link:hover { color: #00ff00; }
+ .error { background: #2a1a1a; border: 1px solid #aa0000; color: #aa0000; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ .info { background: #1a2a2a; border: 1px solid #0088aa; color: #0088aa; padding: 15px; border-radius: 8px; margin: 20px 0; }
+ .article-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 10px; }
+ .subject-highlight { font-size: 1.1em; color: #00ff00; font-weight: bold; }
+ .quoted-text {
+ color: #888888;
+ font-style: italic;
+ border-left: 2px solid #666;
+ padding-left: 8px;
+ margin-left: 4px;
+ display: block;
+ }
+ .signature {
+ color: #666666;
+ border-top: 1px solid #333;
+ margin-top: 10px;
+ padding-top: 10px;
+ font-size: 0.9em;
+ display: block;
+ }
+ .url-link {
+ color: #00aaff;
+ text-decoration: underline;
+ }
+ .reply-section {
+ background: #1a1a1a;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ border: 1px solid #333;
+ text-align: center;
+ }
+ .reply-button {
+ display: inline-block;
+ background: #2a2a4a;
+ color: #00aaff;
+ text-decoration: none;
+ padding: 12px 24px;
+ border-radius: 6px;
+ font-weight: bold;
+ margin: 10px 0;
+ border: 2px solid #0088aa;
+ transition: all 0.3s ease;
+ }
+ .reply-button:hover {
+ background: #3a3a6a;
+ border-color: #00aaff;
+ box-shadow: 0 0 10px rgba(0, 170, 255, 0.3);
+ }
+ .reply-note {
+ font-size: 0.9em;
+ color: #888;
+ margin-top: 15px;
+ text-align: left;
+ background: #0a0a0a;
+ padding: 10px;
+ border-radius: 4px;
+ border-left: 3px solid #00aaff;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="header">
+ <h1>๐Ÿ“ฐ Article {{.ArticleNumber}}</h1>
+ <p><a href="/" class="back-link">โ† Back to Home</a> |
+ <a href="/group/{{.GroupName}}" class="back-link">โ† Back to {{.GroupName}}</a> |
+ <a href="/active" class="back-link">๐Ÿ“‹ All Groups</a></p>
+ </div>
+
+ {{if not .ConnectionError}}
+ <div class="reply-section">
+ <h3>๐Ÿ’ฌ Reply to this Article</h3>
+ <p>Reply to article by <strong>{{.From}}</strong> in <strong>{{.GroupName}}</strong></p>
+ <a href="http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880/" target="_blank" class="reply-button">
+ ๐Ÿ’ฌ OPEN POSTING SERVICE
+ </a>
+ <p class="reply-note">
+ <strong>โ„น๏ธ For Your Reply:</strong><br>
+ {{if .Subject}}โ€ข <strong>Subject:</strong> {{.Subject}}<br>{{end}}
+ {{if .MessageID}}โ€ข <strong>References:</strong> {{.MessageID}}<br>{{end}}
+ โ€ข <strong>Newsgroups:</strong> {{.GroupName}}<br>
+ โ€ข <strong>Original Author:</strong> {{.From}}<br>
+ โ€ข Use <strong>Tor Browser</strong> to access posting service
+ </p>
+ </div>
+ {{end}}
+
+ {{if .ConnectionError}}
+ <div class="error">
+ <h3>โŒ Connection Error</h3>
+ <p>{{.ConnectionError}}</p>
+ <p><a href="/health" style="color: #00aaff;">Check system health</a> |
+ <a href="/group/{{.GroupName}}" style="color: #00aaff;">Back to group</a></p>
+ </div>
+ {{else}}
+
+ <div class="article-headers">
+ <h3>๐Ÿ“‹ Article Headers</h3>
+ <div class="article-meta">
+ {{if .Subject}}
+ <div class="header-item">
+ <span class="header-label">Subject:</span><br>
+ <span class="subject-highlight">{{.Subject}}</span>
+ </div>
+ {{end}}
+
+ {{if .From}}
+ <div class="header-item">
+ <span class="header-label">From:</span><br>
+ <span class="header-value">{{.From}}</span>
+ </div>
+ {{end}}
+
+ {{if .Date}}
+ <div class="header-item">
+ <span class="header-label">Date:</span><br>
+ <span class="header-value">{{.Date}}</span>
+ </div>
+ {{end}}
+
+ {{if .MessageID}}
+ <div class="header-item">
+ <span class="header-label">Message-ID:</span><br>
+ <span class="header-value">{{.MessageID}}</span>
+ </div>
+ {{end}}
+
+ {{if .Newsgroups}}
+ <div class="header-item">
+ <span class="header-label">Newsgroups:</span><br>
+ <span class="header-value">{{.Newsgroups}}</span>
+ </div>
+ {{end}}
+
+ {{if .References}}
+ <div class="header-item">
+ <span class="header-label">References:</span><br>
+ <span class="header-value" style="font-size: 0.8em; word-break: break-all;">{{.References}}</span>
+ </div>
+ {{end}}
+ </div>
+
+ {{if .AllHeaders}}
+ <details style="margin-top: 15px;">
+ <summary style="color: #ffaa00; cursor: pointer;">๐Ÿ” Show All Headers</summary>
+ <div style="margin-top: 10px; background: #0a0a0a; padding: 10px; border-radius: 5px;">
+ {{range $key, $value := .AllHeaders}}
+ <div style="margin: 2px 0; font-size: 0.9em;">
+ <strong style="color: #ffaa00;">{{$key}}:</strong>
+ <span style="color: #cccccc; word-break: break-word;">{{$value}}</span>
+ </div>
+ {{end}}
+ </div>
+ </details>
+ {{end}}
+ </div>
+
+ <div class="article-body">
+ <h3>๐Ÿ“„ Article Content</h3>
+ {{if .Body}}
+ <div class="body-content">{{.Body}}</div>
+ {{else}}
+ <div class="warning">
+ <p>โš ๏ธ Article body is empty or could not be retrieved.</p>
+ </div>
+ {{end}}
+ </div>
+
+ <div class="info">
+ <h3>๐Ÿ’ก Navigation Tips</h3>
+ <ul>
+ <li><strong>Use browser back button</strong> to return to group view</li>
+ <li><strong>References header</strong> shows message threading</li>
+ <li><strong>Message-ID</strong> is unique identifier for this article</li>
+ <li><strong>Quoted text</strong> appears dimmed and indented</li>
+ <li><strong>Signatures</strong> appear separated and dimmed</li>
+ </ul>
+ </div>
+
+ {{end}}
+ </div>
+</body>
+</html>`
+
+ tmpl, _ := template.New("article").Parse(articleHTML)
+
+ // Process body for better display with proper HTML escaping
+ var bodyHTML template.HTML
+ var quotedBody string // For reply functionality
+ if len(body) > 0 {
+ bodyText := strings.Join(body, "\n")
+
+ // Create quoted version for replies (add > to each line)
+ quotedLines := strings.Split(bodyText, "\n")
+ for i, line := range quotedLines {
+ quotedLines[i] = "> " + line
+ }
+ quotedBody = strings.Join(quotedLines, "\n")
+
+ // Prima escape tutto l'HTML per sicurezza
+ bodyText = html.EscapeString(bodyText)
+
+ // Poi aggiungi styling per quotes e signature
+ lines := strings.Split(bodyText, "\n")
+ inSignature := false
+
+ for i, line := range lines {
+ trimmedLine := strings.TrimSpace(line)
+
+ // Detect signature start (-- line)
+ if trimmedLine == "--" && !inSignature {
+ inSignature = true
+ lines[i] = `<div class="signature">` + line
+ continue
+ }
+
+ // Se siamo in signature, continua lo styling
+ if inSignature {
+ if i == len(lines)-1 {
+ lines[i] = line + `</div>` // Chiudi signature alla fine
+ }
+ continue
+ }
+
+ // Quote detection (linee che iniziano con >)
+ if strings.HasPrefix(trimmedLine, "&gt;") {
+ lines[i] = `<div class="quoted-text">` + line + `</div>`
+ }
+
+ // URL detection semplice
+ if strings.Contains(line, "http://") || strings.Contains(line, "https://") || strings.Contains(line, "ftp://") {
+ // Non implementiamo link cliccabili per sicurezza, ma evidenziamo
+ lines[i] = strings.ReplaceAll(line, "http://", `<span class="url-link">http://`)
+ lines[i] = strings.ReplaceAll(lines[i], "https://", `<span class="url-link">https://`)
+ lines[i] = strings.ReplaceAll(lines[i], "ftp://", `<span class="url-link">ftp://`)
+ // Chiudi span alla fine della linea se contiene URL
+ if strings.Contains(lines[i], `<span class="url-link">`) {
+ lines[i] += `</span>`
+ }
+ }
+ }
+
+ // Se signature non รจ stata chiusa, chiudila
+ if inSignature && !strings.Contains(lines[len(lines)-1], "</div>") {
+ lines[len(lines)-1] += `</div>`
+ }
+
+ processedBody := strings.Join(lines, "\n")
+
+ // Marca come HTML sicuro
+ bodyHTML = template.HTML(processedBody)
+ }
+
+ data := map[string]interface{}{
+ "GroupName": groupName,
+ "ArticleNumber": articleNumber,
+ "ConnectionError": connectionError,
+ "Body": bodyHTML, // Usa bodyHTML invece di bodyText
+ "AllHeaders": headers,
+ "EncodedGroupName": url.QueryEscape(groupName),
+ "QuotedBody": quotedBody,
+ }
+
+ // Extract common headers for easy access and URL encode them
+ if headers != nil {
+ if subject, ok := headers["Subject"]; ok {
+ data["Subject"] = subject
+ // Add "Re: " if not already present for reply functionality
+ replySubject := subject
+ // Check if subject already starts with "Re:" (case insensitive, handle spaces)
+ subjectLower := strings.ToLower(strings.TrimSpace(subject))
+ if !strings.HasPrefix(subjectLower, "re:") {
+ replySubject = "Re: " + subject
+ }
+ data["EncodedSubject"] = url.QueryEscape(replySubject)
+ }
+ if from, ok := headers["From"]; ok {
+ data["From"] = from
+ }
+ if date, ok := headers["Date"]; ok {
+ data["Date"] = date
+ }
+ if msgID, ok := headers["Message-ID"]; ok {
+ data["MessageID"] = msgID
+ // Ensure Message-ID has proper < > format for References
+ properMsgID := msgID
+ if !strings.HasPrefix(msgID, "<") {
+ properMsgID = "<" + msgID
+ }
+ if !strings.HasSuffix(properMsgID, ">") {
+ properMsgID = properMsgID + ">"
+ }
+ data["EncodedMessageID"] = url.QueryEscape(properMsgID)
+ }
+ if newsgroups, ok := headers["Newsgroups"]; ok {
+ data["Newsgroups"] = newsgroups
+ }
+ if references, ok := headers["References"]; ok {
+ data["References"] = references
+ }
+ }
+
+ // Prepare quoted content for reply
+ if quotedBody != "" && headers != nil {
+ if from, ok := headers["From"]; ok {
+ if date, ok := headers["Date"]; ok {
+ quotedContent := fmt.Sprintf("On %s, %s wrote:\n\n%s", date, from, quotedBody)
+ data["EncodedQuotedContent"] = url.QueryEscape(quotedContent)
+ }
+ }
+ }
+
+ tmpl.Execute(w, data)
+}
+
+func main() {
+ log.Printf("๐Ÿ“ Logging initialized - writing to stdout and %s", "/var/log/onion-newsreader/onion-newsreader.log")
+ log.Printf("๐Ÿง… Onion Newsreader starting...")
+
+ server := NewNewsServer()
+ defer server.client.Close()
+
+ log.Printf("๐Ÿš€ Starting Onion Newsreader HTTP server...")
+
+ http.HandleFunc("/", server.handleHome)
+ http.HandleFunc("/health", server.handleHealth)
+ http.HandleFunc("/test-onion", server.handleTestOnion)
+ http.HandleFunc("/search", server.handleSearch)
+ http.HandleFunc("/active", server.handleActive)
+ http.HandleFunc("/download-status", server.handleDownloadStatus)
+ http.HandleFunc("/group/", server.handleGroup)
+ http.HandleFunc("/article/", server.handleArticle)
+
+ log.Printf("โœ… HTTP server initialization complete")
+ log.Printf("๐Ÿ”„ Starting background NNTP connection routine...")
+
+ go func() {
+ for {
+ time.Sleep(10 * time.Second)
+ log.Printf("๐Ÿ”„ Attempting NNTP connection...")
+ if err := server.client.ensureConnected(); err != nil {
+ log.Printf("โš ๏ธ NNTP connection failed: %v", err)
+ log.Printf("โฐ Retrying NNTP connection in 120 seconds...")
+ time.Sleep(2 * time.Minute)
+ } else {
+ log.Printf("โœ… NNTP connection established successfully")
+ break
+ }
+ }
+ }()
+
+ log.Printf("๐Ÿš€ Starting HTTP server on 127.0.0.1:8080")
+ log.Printf("๐Ÿ“ก Target NNTP: peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119")
+ log.Printf("๐Ÿ”„ Fallback NNTP: news.tcpreset.net:119 (anonymous access)")
+ log.Printf("โœ๏ธ Posting URL: http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880/")
+ log.Printf("๐Ÿ”’ Stealth mode: true")
+ log.Printf("๐Ÿ’ฌ Support contact: info@tcpreset.net")
+ log.Printf("๐ŸŒ Web interface ready - NNTP connection will be established in background")
+
+ if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
+ log.Fatalf("โŒ HTTP server failed: %v", err)
+ }
+}