diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..5147ff4 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,12 @@ +run: + deadline: 5m +linters: + enable-all: true + disable: + # Actually not really needed. + - gochecknoglobals +linters-settings: + lll: + line-length: 420 + gocyclo: + min-complexity: 40 \ No newline at end of file diff --git a/commands/exported.go b/commands/exported.go new file mode 100644 index 0000000..d2e02e8 --- /dev/null +++ b/commands/exported.go @@ -0,0 +1,17 @@ +package commands + +import ( + // stdlib + "log" + + // local + "develop.pztrn.name/gonews/gonews/commands/greeting" + "develop.pztrn.name/gonews/gonews/commands/quit" +) + +func Initialize() { + log.Println("Initializing commands...") + + greeting.Initialize() + quit.Initialize() +} diff --git a/commands/greeting/exported.go b/commands/greeting/exported.go new file mode 100644 index 0000000..bfe92d8 --- /dev/null +++ b/commands/greeting/exported.go @@ -0,0 +1,23 @@ +package greeting + +import ( + // stdlib + "log" + + // local + "develop.pztrn.name/gonews/gonews/eventer" + "develop.pztrn.name/gonews/gonews/networker" +) + +func Initialize() { + log.Println("Initializing greeting command...") + + eventer.AddEventHandler(&eventer.EventHandler{ + Command: "internal/greeting", + Handler: handler, + }) +} + +func handler(data interface{}) interface{} { + return &networker.Reply{Code: "201", Data: "NNTP server is ready, posting prohibited\r\n"} +} diff --git a/commands/quit/exported.go b/commands/quit/exported.go new file mode 100644 index 0000000..9276fbd --- /dev/null +++ b/commands/quit/exported.go @@ -0,0 +1,23 @@ +package quit + +import ( + // stdlib + "log" + + // local + "develop.pztrn.name/gonews/gonews/eventer" + "develop.pztrn.name/gonews/gonews/networker" +) + +func Initialize() { + log.Println("Initializing quit command...") + + eventer.AddEventHandler(&eventer.EventHandler{ + Command: "commands/quit", + Handler: handler, + }) +} + +func handler(data interface{}) interface{} { + return &networker.Reply{Code: "205", Data: "NNTP Service exits normally\r\n"} +} diff --git a/configuration/config.go b/configuration/config.go new file mode 100644 index 0000000..ae28e97 --- /dev/null +++ b/configuration/config.go @@ -0,0 +1,18 @@ +package configuration + +// Represents configuration file structure. +type config struct { + // Network represents network stack configuration. + Network []Network `yaml:"network"` +} + +type Network struct { + // Address represents address to bing in form of "ip:port". + Address string `yaml:"address"` + // Limit sets maximum simultaneous connections that can be + // processed by worker. + Limit int `yaml:"limit"` + // Type sets connection type. See networker for available + // types. + Type string `yaml:"type"` +} diff --git a/configuration/exported.go b/configuration/exported.go new file mode 100644 index 0000000..e9af5f4 --- /dev/null +++ b/configuration/exported.go @@ -0,0 +1,55 @@ +package configuration + +import ( + // stdlib + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + // other + "gopkg.in/yaml.v2" +) + +var ( + Cfg *config +) + +// Initialize initializes package and parses configuration into struct. +func Initialize() { + log.Println("Initializing configuration...") + + pathRaw, found := os.LookupEnv("GONEWS_CONFIG") + if !found { + log.Fatalln("Failed to read configuration - no GONEWS_CONFIG environment variable defined.") + } + + // Normalize path. + if strings.HasPrefix(pathRaw, "~") { + userHomeDir, err := os.UserHomeDir() + if err != nil { + log.Fatalln("Failed to obtain user's home directory path: " + err.Error()) + } + + pathRaw = strings.Replace(pathRaw, "~", userHomeDir, 1) + } + absPath, err1 := filepath.Abs(pathRaw) + if err1 != nil { + log.Fatalln("Failed to get absolute path for configuration file: " + err1.Error()) + } + + // Read and parse configuration file. + fileData, err2 := ioutil.ReadFile(absPath) + if err2 != nil { + log.Fatalln("Failed to read configuration file data: " + err2.Error()) + } + + Cfg = &config{} + err3 := yaml.Unmarshal(fileData, Cfg) + if err3 != nil { + log.Fatalln("Failed to parse configuration file: " + err3.Error()) + } + + log.Printf("Configuration file parsed: %+v\n", Cfg) +} diff --git a/networker/connection.go b/networker/connection.go new file mode 100644 index 0000000..6701553 --- /dev/null +++ b/networker/connection.go @@ -0,0 +1,94 @@ +package networker + +import ( + // stdlib + "bufio" + "log" + "net" + "strings" + + // local + "develop.pztrn.name/gonews/gonews/eventer" +) + +// This function is a connection worker. +func connectionWorker(conn net.Conn) { + remoteAddr := 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) + + // Send greeting. + greetingData, _ := eventer.LaunchEvent("internal/greeting", nil) + greetingReply := greetingData.(*Reply) + + _, err := w.WriteString(greetingReply.Code + " " + greetingReply.Data) + if err != nil { + log.Println("Failed to write greeting for " + remoteAddr.String() + ": " + err.Error()) + return + } + w.Flush() + + // Start reading for commands. + // Every command can be represented as slice where first element + // is actual command and all next - parameters. + // By default we read only one line per iteration. + // ToDo: multiline data parser for posting. + for { + dataAppeared := scanr.Scan() + if !dataAppeared { + log.Println("Failed to read data from " + remoteAddr.String() + ": " + scanr.Err().Error()) + break + } + + log.Println("Got data: " + scanr.Text()) + + // ToDo: what if we'll upload binary data here? + // Not supported yet. + data := strings.Split(scanr.Text(), " ") + replyRaw, err := eventer.LaunchEvent("commands/"+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()) + } + + // 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") + if err != nil { + log.Println("Failed to write string to socket for " + remoteAddr.String() + ": " + err.Error()) + break + } + w.Flush() + continue + } + + // Every reply will be a reply struct. + reply := replyRaw.(*Reply) + + _, err1 := w.WriteString(reply.Code + " " + reply.Data) + if err1 != nil { + log.Println("Failed to write string to socket for " + remoteAddr.String() + ": " + err1.Error()) + break + } + w.Flush() + + // Check for QUIT command. + if strings.ToLower(data[0]) == "quit" { + log.Println("QUIT command received, closing connection to " + remoteAddr.String()) + break + } + } +} diff --git a/networker/errors.go b/networker/errors.go new file mode 100644 index 0000000..93a92a0 --- /dev/null +++ b/networker/errors.go @@ -0,0 +1,6 @@ +package networker + +const ( + unknownCommandErrorCode = "500" + unknownCommandErrorText = "Unknown command" +) diff --git a/networker/reply.go b/networker/reply.go new file mode 100644 index 0000000..841922c --- /dev/null +++ b/networker/reply.go @@ -0,0 +1,6 @@ +package networker + +type Reply struct { + Code string + Data string +} diff --git a/networker/server.go b/networker/server.go new file mode 100644 index 0000000..9b57e2c --- /dev/null +++ b/networker/server.go @@ -0,0 +1,37 @@ +package networker + +import ( + // stdlib + "log" + "net" + + // local + "develop.pztrn.name/gonews/gonews/configuration" +) + +// 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 + ")") + + l, err := net.Listen("tcp", config.Address) + if err != nil { + log.Fatalln("Failed to start TCP server on " + config.Address + ": " + err.Error()) + } + defer func() { + err := l.Close() + if err != nil { + log.Println("Failed to close TCP server on " + config.Address + ": " + err.Error()) + } + }() + + for { + conn, err1 := l.Accept() + if err1 != nil { + log.Println("Failed to accept new incoming connection: " + err1.Error()) + continue + } + + go connectionWorker(conn) + } +}