diff --git a/cmd/yans/main.go b/cmd/yans/main.go index ddc8e24..538d10d 100644 --- a/cmd/yans/main.go +++ b/cmd/yans/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "github.com/ChronosX88/yans/internal/common" "github.com/ChronosX88/yans/internal/config" "github.com/ChronosX88/yans/internal/server" "log" @@ -26,7 +27,7 @@ func main() { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) - log.Println("Starting YANS...") + log.Printf("Starting %s...", common.ServerName) ns, err := server.NewNNTPServer(cfg) if err != nil { log.Fatal(err) @@ -35,10 +36,11 @@ func main() { if err := ns.Start(); err != nil { log.Fatal(err) } - log.Println("YANS has been successfully started!") + log.Printf("%s has been successfully started!", common.ServerName) + log.Printf("Version: %s", common.ServerVersion) for range c { - log.Println("Stopping YANS...") + log.Printf("Stopping %s...", common.ServerName) ns.Stop() break } diff --git a/internal/common/const.go b/internal/common/const.go new file mode 100644 index 0000000..6e6824b --- /dev/null +++ b/internal/common/const.go @@ -0,0 +1,6 @@ +package common + +const ( + ServerName = "YANS" + ServerVersion = "0.0.1" +) diff --git a/internal/protocol/capabilities.go b/internal/protocol/capabilities.go new file mode 100644 index 0000000..d7f04c8 --- /dev/null +++ b/internal/protocol/capabilities.go @@ -0,0 +1,80 @@ +package protocol + +import ( + "fmt" + "strings" +) + +type CapabilityType int + +const ( + VersionCapability CapabilityType = iota + ReaderCapability + IHaveCapability + PostCapability + NewNewsCapability + HdrCapability + OverCapability + ListCapability + ImplementationCapability + ModeReaderCapability +) + +func (ct CapabilityType) String() string { + switch ct { + case VersionCapability: + return CapabilityNameVersion + case ReaderCapability: + return CapabilityNameReader + case IHaveCapability: + return CapabilityNameIHave + case PostCapability: + return CapabilityNamePost + case NewNewsCapability: + return CapabilityNameNewNews + case HdrCapability: + return CapabilityNameHdr + case OverCapability: + return CapabilityNameOver + case ListCapability: + return CapabilityNameList + case ImplementationCapability: + return CapabilityNameImplementation + case ModeReaderCapability: + return CapabilityNameModeReader + default: + return "" + } +} + +type Capability struct { + Type CapabilityType + Params string // optional +} + +type Capabilities []Capability + +func (cs Capabilities) Add(c Capability) { + for _, v := range cs { + if v.Type == c.Type { + return // allowed only unique items + } + } + cs = append(cs, c) +} + +func (cs Capabilities) String() string { + sb := strings.Builder{} + sb.Write([]byte("101 Capability list:" + CRLF)) + + for _, v := range cs { + if v.Params != "" { + sb.Write([]byte(fmt.Sprintf("%s %s%s", v.Type, v.Params, CRLF))) + } else { + sb.Write([]byte(fmt.Sprintf("%s%s", v.Type, CRLF))) + } + } + sb.Write([]byte(MultilineEnding)) + + return sb.String() +} diff --git a/internal/protocol/constants.go b/internal/protocol/constants.go index cb022e9..68ba9e6 100644 --- a/internal/protocol/constants.go +++ b/internal/protocol/constants.go @@ -1,5 +1,10 @@ package protocol +const ( + CRLF = "\r\n" + MultilineEnding = "." +) + const ( CommandCapabilities = "CAPABILITIES" CommandQuit = "QUIT" @@ -9,9 +14,23 @@ const ( ) const ( - MessageNNTPServiceReadyPostingProhibited = "201 YANS NNTP Service Ready, posting prohibited\n" + CapabilityNameVersion = "VERSION" + CapabilityNameReader = "READER" + CapabilityNameIHave = "IHAVE" + CapabilityNamePost = "POST" + CapabilityNameNewNews = "NEWNEWS" + CapabilityNameHdr = "HDR" + CapabilityNameOver = "OVER" + CapabilityNameList = "LIST" + CapabilityNameImplementation = "IMPLEMENTATION" + CapabilityNameModeReader = "MODE-READER" +) + +const ( + MessageNNTPServiceReadyPostingProhibited = "201 YANS NNTP Service Ready, posting prohibited" MessageReaderModePostingProhibited = "201 Reader mode, posting prohibited" MessageNNTPServiceExitsNormally = "205 NNTP Service exits normally" MessageUnknownCommand = "500 Unknown command" MessageErrorHappened = "403 Failed to process command: " + MessageListOfNewsgroupsFollows = "215 list of newsgroups follows" ) diff --git a/internal/server/nntp_server.go b/internal/server/nntp_server.go index 6b4eba3..bb4fbda 100644 --- a/internal/server/nntp_server.go +++ b/internal/server/nntp_server.go @@ -1,10 +1,10 @@ package server import ( - "bufio" "context" "fmt" "github.com/ChronosX88/yans/internal" + "github.com/ChronosX88/yans/internal/common" "github.com/ChronosX88/yans/internal/config" "github.com/ChronosX88/yans/internal/models" "github.com/ChronosX88/yans/internal/protocol" @@ -14,10 +14,20 @@ import ( "io" "log" "net" + "net/textproto" "strings" "time" ) +var ( + Capabilities = protocol.Capabilities{ + {Type: protocol.VersionCapability, Params: "2"}, + {Type: protocol.ImplementationCapability, Params: fmt.Sprintf("%s %s", common.ServerName, common.ServerVersion)}, + {Type: protocol.ModeReaderCapability}, + {Type: protocol.ListCapability, Params: "ACTIVE NEWSGROUPS"}, + } +) + type NNTPServer struct { ctx context.Context cancelFunc context.CancelFunc @@ -81,19 +91,21 @@ func (ns *NNTPServer) Start() error { } func (ns *NNTPServer) handleNewConnection(ctx context.Context, conn net.Conn) { - _, err := conn.Write([]byte(protocol.MessageNNTPServiceReadyPostingProhibited)) + _, err := conn.Write([]byte(protocol.MessageNNTPServiceReadyPostingProhibited + protocol.CRLF)) if err != nil { log.Print(err) conn.Close() return } + + tconn := textproto.NewConn(conn) for { select { case <-ctx.Done(): break default: { - message, err := bufio.NewReader(conn).ReadString('\n') + message, err := tconn.ReadLine() if err != nil { if err == io.EOF || err.(*net.OpError).Unwrap() == net.ErrClosed { log.Printf("Client %s has diconnected!", conn.RemoteAddr().String()) @@ -104,7 +116,7 @@ func (ns *NNTPServer) handleNewConnection(ctx context.Context, conn net.Conn) { return } log.Printf("Received message from %s: %s", conn.RemoteAddr().String(), string(message)) - err = ns.handleMessage(conn, message) + err = ns.handleMessage(tconn, message) if err != nil { log.Print(err) conn.Close() @@ -115,8 +127,7 @@ func (ns *NNTPServer) handleNewConnection(ctx context.Context, conn net.Conn) { } } -func (ns *NNTPServer) handleMessage(conn net.Conn, msg string) error { - msg = strings.TrimSuffix(msg, "\r\n") +func (ns *NNTPServer) handleMessage(conn *textproto.Conn, msg string) error { splittedMessage := strings.Split(msg, " ") command := splittedMessage[0] @@ -126,7 +137,7 @@ func (ns *NNTPServer) handleMessage(conn net.Conn, msg string) error { switch command { case protocol.CommandCapabilities: { - reply = "101 Capability list:\r\nVERSION 2\r\nIMPLEMENTATION\r\n." + reply = Capabilities.String() break } case protocol.CommandDate: @@ -158,11 +169,11 @@ func (ns *NNTPServer) handleMessage(conn net.Conn, msg string) error { log.Println(err) } sb := strings.Builder{} - sb.Write([]byte("215 list of newsgroups follows\n")) + sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + protocol.CRLF)) if len(splittedMessage) == 1 || splittedMessage[1] == "ACTIVE" { for _, v := range groups { // TODO set high/low mark and posting status to actual values - sb.Write([]byte(fmt.Sprintf("%s 0 0 n\r\n", v.GroupName))) + sb.Write([]byte(fmt.Sprintf("%s 0 0 n"+protocol.CRLF, v.GroupName))) } } else if splittedMessage[1] == "NEWSGROUPS" { for _, v := range groups { @@ -189,7 +200,7 @@ func (ns *NNTPServer) handleMessage(conn net.Conn, msg string) error { } } - _, err := conn.Write([]byte(reply + "\r\n")) + err := conn.PrintfLine(reply) if quit { conn.Close() }