From f5b1f9d9f93fb98b1e150b538bf48115cd856c18 Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Tue, 10 Sep 2019 01:07:36 +0500 Subject: [PATCH] Capabilities now per-connection thing, new connection structure and proper MODE READER stub. --- commands/capabilities/exported.go | 24 ++++-- networker/connection.go | 136 +++++++++++++++++++++++------- networker/server.go | 12 ++- 3 files changed, 133 insertions(+), 39 deletions(-) diff --git a/commands/capabilities/exported.go b/commands/capabilities/exported.go index d7b8fa1..2280bd1 100644 --- a/commands/capabilities/exported.go +++ b/commands/capabilities/exported.go @@ -6,7 +6,6 @@ import ( // local "develop.pztrn.name/gonews/gonews/eventer" - "develop.pztrn.name/gonews/gonews/networker" ) var capabilities = []string{ @@ -16,13 +15,14 @@ var capabilities = []string{ func Initialize() { log.Println("Initializing capabilities command...") + // Global capabilities adder. eventer.AddEventHandler(&eventer.EventHandler{ Command: "internal/capability_add", Handler: addCapability, }) eventer.AddEventHandler(&eventer.EventHandler{ - Command: "commands/capabilities", + Command: "internal/capabilities", Handler: handler, }) } @@ -33,12 +33,18 @@ func addCapability(data interface{}) interface{} { return nil } -func handler(data interface{}) interface{} { - dataToReturn := "Capability list:\r\n" +//func handler(data interface{}) interface{} { +// dataToReturn := "Capability list:\r\n" +// +// for _, cap := range capabilities { +// dataToReturn += cap + "\r\n" +// } +// dataToReturn += ".\r\n" +// return &networker.Reply{Code: "101", Data: dataToReturn} +//} - for _, cap := range capabilities { - dataToReturn += cap + "\r\n" - } - dataToReturn += ".\r\n" - return &networker.Reply{Code: "101", Data: dataToReturn} +func handler(data interface{}) interface{} { + caps := make([]string, len(capabilities)) + copy(caps, capabilities) + return caps } diff --git a/networker/connection.go b/networker/connection.go index f97eaec..e44911b 100644 --- a/networker/connection.go +++ b/networker/connection.go @@ -11,34 +11,73 @@ import ( "develop.pztrn.name/gonews/gonews/eventer" ) -// This function is a connection worker. -func connectionWorker(conn net.Conn) { - remoteAddr := conn.RemoteAddr() +// This structure represents single NNTP client connection. +type connection struct { + // Connection details and handlers. + conn net.Conn + remoteAddr net.Addr + + // Read and write buffers + reader *bufio.Reader + writer *bufio.Writer + // Scanner that helps us to read incoming data. + readScanner *bufio.Scanner + + // Connection flags. + // Are we in READER or MODE-READER (transit) mode? + // Right now transit mode isn't implemented. Implementation will + // require start using two goroutines for handling connections, + // one for writing and one for reading. + transit bool + // Connection capabilites. + capabilities []string +} + +// Initialize initializes necessary things. +func (c *connection) Initialize(conn net.Conn) { + c.conn = conn + c.remoteAddr = c.conn.RemoteAddr() + log.Printf("accepted connection from %v\n", conn.RemoteAddr()) - defer func() { - err := conn.Close() - if err != nil { - log.Println("Failed to close connection from " + remoteAddr.String() + ": " + err.Error()) - } - log.Println("Connection from " + remoteAddr.String() + " closed") - }() - // Create buffers. - r := bufio.NewReader(conn) - w := bufio.NewWriter(conn) - scanr := bufio.NewScanner(r) + c.reader = bufio.NewReader(conn) + c.writer = bufio.NewWriter(conn) + c.readScanner = bufio.NewScanner(c.reader) + + // Get capabilities for this connection. + caps, _ := eventer.LaunchEvent("internal/capabilities", nil) + c.capabilities = caps.([]string) + + // Set transit mode by default, according to RFC. + c.transit = true +} + +// Start starts working with connection. Should be launched in separate +// goroutine. +// It will send greeting and then falls into infinite loop for working +// with connection until the end. +// Right now it implements only READER mode, no transit (which is used +// by server-to-server peering extensively). +func (c *connection) Start() { + defer func() { + err := c.conn.Close() + if err != nil { + log.Println("Failed to close connection from " + c.remoteAddr.String() + ": " + err.Error()) + } + log.Println("Connection from " + c.remoteAddr.String() + " closed") + }() // Send greeting. greetingData, _ := eventer.LaunchEvent("internal/greeting", nil) greetingReply := greetingData.(*Reply) - _, err := w.WriteString(greetingReply.Code + " " + greetingReply.Data) + _, err := c.writer.WriteString(greetingReply.Code + " " + greetingReply.Data) if err != nil { - log.Println("Failed to write greeting for " + remoteAddr.String() + ": " + err.Error()) + log.Println("Failed to write greeting for " + c.remoteAddr.String() + ": " + err.Error()) return } - w.Flush() + c.writer.Flush() // Start reading for commands. // Every command can be represented as slice where first element @@ -46,48 +85,87 @@ func connectionWorker(conn net.Conn) { // By default we read only one line per iteration. // ToDo: multiline data parser for posting. for { - dataAppeared := scanr.Scan() + dataAppeared := c.readScanner.Scan() if !dataAppeared { - log.Println("Failed to read data from " + remoteAddr.String() + ": " + scanr.Err().Error()) + log.Println("Failed to read data from " + c.remoteAddr.String() + ": " + c.readScanner.Err().Error()) break } - log.Println("Got data: " + scanr.Text()) + log.Println("Got data: " + c.readScanner.Text()) // ToDo: what if we'll upload binary data here? // Not supported yet. - data := strings.Split(scanr.Text(), " ") + data := strings.Split(c.readScanner.Text(), " ") + + // Separate capabilities worker. + if strings.ToLower(data[0]) == "capabilities" { + dataToWrite := "Capability list:\r\n" + + for _, cap := range c.capabilities { + dataToWrite += cap + "\r\n" + } + + // We're also mode-switching server (in future), so we should + // also be aware of mode reader things. Writing to client will + // depend on c.transit variable. + // We will announce MODE-READER capability after initial + // connection (because we're in transit mode by default, according + // to RFC), and when client issue "MODE READER" command we will + // stop announcing MODE-READER and will start announce READER + // capability. + if c.transit { + dataToWrite += "MODE-READER\r\n" + } else { + dataToWrite += "READER\r\n" + } + + dataToWrite += ".\r\n" + c.writer.WriteString(dataToWrite) + c.writer.Flush() + continue + } + + // Mode worker. Reader only now. + if strings.ToLower(data[0]) == "mode" && strings.ToLower(data[1]) == "reader" { + c.transit = false + // In any case we'll require user authentication for posting. + c.writer.WriteString("201 Posting prohibited\r\n") + c.writer.Flush() + continue + } + + // Execute passed command. replyRaw, err := eventer.LaunchEvent("commands/"+strings.ToLower(data[0]), data[1:]) if err != nil { // We won't break here as this is just logging of appeared error. - log.Println("Error appeared while processing command '" + data[0] + "' for " + remoteAddr.String() + ": " + err.Error()) + log.Println("Error appeared while processing command '" + data[0] + "' for " + c.remoteAddr.String() + ": " + err.Error()) } // We might have nil in reply, so we'll assume that passed command // is unknown to us. if replyRaw == nil { - _, err := w.WriteString(unknownCommandErrorCode + " " + unknownCommandErrorText + "\r\n") + _, err := c.writer.WriteString(unknownCommandErrorCode + " " + unknownCommandErrorText + "\r\n") if err != nil { - log.Println("Failed to write string to socket for " + remoteAddr.String() + ": " + err.Error()) + log.Println("Failed to write string to socket for " + c.remoteAddr.String() + ": " + err.Error()) break } - w.Flush() + c.writer.Flush() continue } // Every reply will be a reply struct. reply := replyRaw.(*Reply) - _, err1 := w.WriteString(reply.Code + " " + reply.Data) + _, err1 := c.writer.WriteString(reply.Code + " " + reply.Data) if err1 != nil { - log.Println("Failed to write string to socket for " + remoteAddr.String() + ": " + err1.Error()) + log.Println("Failed to write string to socket for " + c.remoteAddr.String() + ": " + err1.Error()) break } - w.Flush() + c.writer.Flush() // Check for QUIT command. if strings.ToLower(data[0]) == "quit" { - log.Println("QUIT command received, closing connection to " + remoteAddr.String()) + log.Println("QUIT command received, closing connection to " + c.remoteAddr.String()) break } } diff --git a/networker/server.go b/networker/server.go index 9b57e2c..2f2e392 100644 --- a/networker/server.go +++ b/networker/server.go @@ -9,11 +9,17 @@ import ( "develop.pztrn.name/gonews/gonews/configuration" ) +var connections map[string]*connection + // This function responsible for accepting incoming connections for // each address configuration. func startServer(config configuration.Network) { log.Println("Starting server on " + config.Address + " (type: " + config.Type + ")") + if connections == nil { + connections = make(map[string]*connection) + } + l, err := net.Listen("tcp", config.Address) if err != nil { log.Fatalln("Failed to start TCP server on " + config.Address + ": " + err.Error()) @@ -32,6 +38,10 @@ func startServer(config configuration.Network) { continue } - go connectionWorker(conn) + c := &connection{} + c.Initialize(conn) + connections[conn.RemoteAddr().String()] = c + + go c.Start() } }