Monday, May 11, 2015

Creating a Simple Golang Network Application, Part 5

After getting the initial networking code active in the part 4, I took the opportunity to make a few tiny changes and clean up some of the code. I removed the line that prints "All channels accounted for" as part of the startup routine in main(). I also reworded the instructions telling the user to type "quit" to exit, and added a few lines to log a "program starting" message so when the program is appending the logfile there's a clear mark of when the application was restarted. But those were really the "inching to the edge of the diving board," as the next thing I wanted to tackle was the logfile output.

The information I wanted to capture is sent to the LogEvent() function. Up until this point all it did was dump information to standard output with fmt.Println then return to the caller.

Here's the new function:


func LogEvent(strFrom string, strMessage string) {

 var strLogFile string
 var err error
 var fileLogFile *os.File

 strLogFile, _ = os.Getwd()
 strLogFile = strLogFile + "/port_listen_logs/port_listen.log"
 if _, err = os.Stat(strLogFile); err == nil {
  fileLogFile, err = os.OpenFile(strLogFile, os.O_APPEND|os.O_RDWR, 0644)
  if err != nil {
   fmt.Println("Log file could not be opened! Error " + err.Error())
   os.Exit(1)
  }  
 } else {
  fileLogFile, err = os.Create(strLogFile)
  if err != nil {
   fmt.Println("Log file could not be created! Error " + err.Error())
   os.Exit(1)
  }
 } 
 defer fileLogFile.Close() 
 fileLogFile.WriteString(time.Now().String() + " From port " + strFrom + ":: " + strMessage + "\n")
}

Some of the code should look familiar; it was  used to set up the previous checks for the existence of the logfile directory. I created a string to hold the logfile name and populated it with the current working directory using os.Getwd(), then in the next line I appended "/port_listen_logs/port_listen.log". This ensures the logfile is created in a directory just under the directory we're running the program from.

The directories were already created moments earlier with the SetupEnvironment() call, so I'm taking the (admittedly lazy and incomplete) path of not checking for possible errors as much as I probably should.

The next thing I want to do is check if the logfile exists; if it doesn't exist, create the file. If it does exist, open it in a mode that allows the application to append to the file.

To do this, I use and if/else code blocks. The first checks the existence of the file using os.Stat(), with the returned value ignored (the underscore ignores the value, but we do capture err.)

If err is nil, that means the file exists so I open the file in read/write using the os library's OpenFile function, which takes the files name (strLogFile), the mode (O_APPEND|O_RDWR), and the permissions (0644). If the error code returned is not nil, we're assuming the file doesn't exist. The else{} block opens the file with the os.Create() call, taking the filename as the argument.

If an error is encountered in either of those file-open attempts, the error is dumped to standard output and the program exits using a call to os.Exit with a nonzero exit code.

The file open code blocks are now finished; the next line calls Close coupled with the keyword defer, meaning that the file will be closed just before the running function returns. No matter where it happens, the file will get closed, and I don't have to remember to do it later.

The last bit is the actual writing to the file using WriteString. It writes the current timestamp, reports the port (passed as an argument to LogEvent), and the message (also passed as an argument.) I added a newline and also used a double-colon to distinguish the information coming from the application from the message being sent to the application. Then the file is closed and the function returns!

Here's the source code as it stands at this point:


package main

import (
 "fmt"
 "strconv"
 "os"
 "strings"
 "net"
 "os/exec"
 "syscall"
 "time"
)

func main() {

 var i uint16
 var strUserInput string
 var intEnvironmentReady int
 chanCommLine := make(chan string)

 intEnvironmentReady = SetupEnvironment()
 if intEnvironmentReady != 0 {
  fmt.Println("Something has gone wrong setting up the system environment, error " + strconv.Itoa(intEnvironmentReady))
  os.Exit(1)
 }
 
 LogEvent("0", "Application initializing ports to monitor...")
 for i = 1; i < 1025; i++ {
  go ListenToPort(i, chanCommLine)
 }
 i = 0
 for {
  strChanData := <-chanCommLine
  if strChanData != "" {
   i++
  }
  if i == 1024 {
   break
  }
 }
 for {
  LogEvent("0", "Application started and monitoring")
  fmt.Println("Type 'quit' and hit enter to exit the program...")
  fmt.Scanf("%s", &strUserInput)
  strUserInput = strings.ToLower(strUserInput)
  if strUserInput == "quit" {
   os.Exit(0)
  }
 }
}
func ListenToPort(uint16Port uint16, chanOutGoing chan<- string) {

 var strPortToListenOn string
 var strRemoteAddr string
 var strRemoteAddrSansPort string

 strPortToListenOn = strconv.FormatInt(int64(uint16Port), 10)

 netListenFor, err := net.Listen("tcp", ":"+strPortToListenOn)
 if err != nil {
  LogEvent(strPortToListenOn, err.Error())
  chanOutGoing <- "done" + strconv.FormatInt(int64(uint16Port), 10)
  return
 }
 chanOutGoing <- "done" + strconv.FormatInt(int64(uint16Port), 10)
 defer netListenFor.Close()

 for {
  conn, err := netListenFor.Accept()
  if err != nil {
   LogEvent(strPortToListenOn, err.Error())
  }
  strRemoteAddr = conn.RemoteAddr().String()
  strRemoteAddrSansPort, _, _ = net.SplitHostPort(strRemoteAddr)
  LogEvent(strPortToListenOn, "Connection attempt made from "+strRemoteAddrSansPort)

  go GrabInput(conn, strPortToListenOn, strRemoteAddrSansPort)
 }
}
func GrabInput(conn net.Conn, strFromListenerNumber string, strClientIP string) {

 var strMessage string

 bufIncoming := make([]byte, 1024)

 for {
  bytesRead, err := conn.Read(bufIncoming)
  if err != nil {
   if err.Error() == "EOF" {
    LogEvent(strFromListenerNumber, "Client located at "+strClientIP+" has disconnected.")
   } else {
    LogEvent(strFromListenerNumber+": Remote IP "+strClientIP, err.Error())
   }
   return
  }
  strMessage = string(bufIncoming[0 : bytesRead-1])
  LogEvent(strFromListenerNumber+": Remote IP "+strClientIP, strMessage)
 }
}
func LogEvent(strFrom string, strMessage string) {

 var strLogFile string
 var err error
 var fileLogFile *os.File

 strLogFile, _ = os.Getwd()
 strLogFile = strLogFile + "/port_listen_logs/port_listen.log"
 if _, err = os.Stat(strLogFile); err == nil {
  fileLogFile, err = os.OpenFile(strLogFile, os.O_APPEND|os.O_RDWR, 0644)
  if err != nil {
   fmt.Println("Log file could not be opened! Error " + err.Error())
   os.Exit(1)
  }  
 } else {
  fileLogFile, err = os.Create(strLogFile)
  if err != nil {
   fmt.Println("Log file could not be created! Error " + err.Error())
   os.Exit(1)
  }
 }
 defer fileLogFile.Close()
 fileLogFile.WriteString(time.Now().String() + " From port " + strFrom + ":: " + strMessage + "\n")
}
func SetupEnvironment() int {

 var strSudoUserUid string
 var strSudoUserGid string
 var intSudoUserUid int
 var intSudoUserGid int  
 var strWorkDir string
 var err error
 var fiDirectory os.FileInfo
 var rLimit syscall.Rlimit

 if os.Getuid() == 0 {
  strSudoUserUid = os.Getenv("SUDO_UID")
  strSudoUserGid = os.Getenv("SUDO_GID")
 } else {
  fmt.Println("Please use sudo to run application (sudo ./port_listen)")
  return 4
 }
 strWorkDir, err = os.Getwd()
 if err != nil {
 fmt.Println("Error getting working directory: " + err.Error())
 }
 strWorkDir = strWorkDir + "/port_listen_logs"
 fiDirectory, err = os.Stat(strWorkDir)
 if err != nil {
  fmt.Println("Logs directory does not exist. Creating directory " + strWorkDir)
  err = os.Mkdir(strWorkDir, 0700)
  if err != nil {
   fmt.Println("Error creating directory; " + err.Error())
   return 5
  }
  intSudoUserUid, _ = strconv.Atoi(strSudoUserUid)
  intSudoUserGid, _ = strconv.Atoi(strSudoUserGid)
  err = os.Chown(strWorkDir, intSudoUserUid, intSudoUserGid)
  if err != nil {
   fmt.Println("Error changing permissions on directory: " + err.Error())
   return 7
  }
 }
 fiDirectory, err = os.Stat(strWorkDir)
 if !fiDirectory.IsDir() {
  fmt.Println(strWorkDir + " is not a directory!")
  return 6
 }

 command := exec.Command("/bin/launchctl", "limit", "maxfiles", "1000000","1000000")
 _ , err = command.Output()
 if err != nil {
  return 1
 }
 err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
 if err != nil {
  return 3
 }
 rLimit.Cur = 1400
 err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
 if err != nil {
  return 2
 }
 return 0
}

But we're not done yet! Go includes a simple tool to format your source code using Go formatting standards. To view what changes would be made to the source code I use:

gofmt -d ./port_listen.go

...whose output is shown in Diff format. To make the changes, I run:

gofmt -l -w -s ./port_listen.go

The changes in this case were very simple, but it was easy to have this automated housekeeping and it made the source file more consistent. My final version:


package main

import (
 "fmt"
 "net"
 "os"
 "os/exec"
 "strconv"
 "strings"
 "syscall"
 "time"
)

func main() {

 var i uint16
 var strUserInput string
 var intEnvironmentReady int
 chanCommLine := make(chan string)

 intEnvironmentReady = SetupEnvironment()
 if intEnvironmentReady != 0 {
  fmt.Println("Something has gone wrong setting up the system environment, error " + strconv.Itoa(intEnvironmentReady))
  os.Exit(1)
 }
 LogEvent("0", "Application initializing ports to monitor...")
 for i = 1; i < 1025; i++ {
  go ListenToPort(i, chanCommLine)
 }
 i = 0
 for {
  strChanData := <-chanCommLine
  if strChanData != "" {
   i++
  }
  if i == 1024 {
   break
  }
 }
 for {
  LogEvent("0", "Application started and monitoring")
  fmt.Println("Type 'quit' and hit enter to exit the program...")
  fmt.Scanf("%s", &strUserInput)
  strUserInput = strings.ToLower(strUserInput)
  if strUserInput == "quit" {
   os.Exit(0)
  }
 }
}
func ListenToPort(uint16Port uint16, chanOutGoing chan<- string) {

 var strPortToListenOn string
 var strRemoteAddr string
 var strRemoteAddrSansPort string

 strPortToListenOn = strconv.FormatInt(int64(uint16Port), 10)

 netListenFor, err := net.Listen("tcp", ":"+strPortToListenOn)
 if err != nil {
  LogEvent(strPortToListenOn, err.Error())
  chanOutGoing <- "done" + strconv.FormatInt(int64(uint16Port), 10)
  return
 }
 chanOutGoing <- "done" + strconv.FormatInt(int64(uint16Port), 10)
 defer netListenFor.Close()

 for {
  conn, err := netListenFor.Accept()
  if err != nil {
   LogEvent(strPortToListenOn, err.Error())
  }
  strRemoteAddr = conn.RemoteAddr().String()
  strRemoteAddrSansPort, _, _ = net.SplitHostPort(strRemoteAddr)
  LogEvent(strPortToListenOn, "Connection attempt made from "+strRemoteAddrSansPort)

  go GrabInput(conn, strPortToListenOn, strRemoteAddrSansPort)
 }
}
func GrabInput(conn net.Conn, strFromListenerNumber string, strClientIP string) {

 var strMessage string

 bufIncoming := make([]byte, 1024)

 for {
  bytesRead, err := conn.Read(bufIncoming)
  if err != nil {
   if err.Error() == "EOF" {
    LogEvent(strFromListenerNumber, "Client located at "+strClientIP+" has disconnected.")
   } else {
    LogEvent(strFromListenerNumber+": Remote IP "+strClientIP, err.Error())
   }
   return
  }
  strMessage = string(bufIncoming[0 : bytesRead-1])
  LogEvent(strFromListenerNumber+": Remote IP "+strClientIP, strMessage)
 }
}
func LogEvent(strFrom string, strMessage string) {

 var strLogFile string
 var err error
 var fileLogFile *os.File

 strLogFile, _ = os.Getwd()
 strLogFile = strLogFile + "/port_listen_logs/port_listen.log"
 if _, err = os.Stat(strLogFile); err == nil {
  fileLogFile, err = os.OpenFile(strLogFile, os.O_APPEND|os.O_RDWR, 0644)
  if err != nil {
   fmt.Println("Log file could not be opened! Error " + err.Error())
   os.Exit(1)
  }
 } else {
  fileLogFile, err = os.Create(strLogFile)
  if err != nil {
   fmt.Println("Log file could not be created! Error " + err.Error())
   os.Exit(1)
  }
 }
 defer fileLogFile.Close()
 fileLogFile.WriteString(time.Now().String() + " From port " + strFrom + ":: " + strMessage + "\n")
}
func SetupEnvironment() int {

 var strSudoUserUid string
 var strSudoUserGid string
 var intSudoUserUid int
 var intSudoUserGid int
 var strWorkDir string
 var err error
 var fiDirectory os.FileInfo
 var rLimit syscall.Rlimit

 if os.Getuid() == 0 {
  strSudoUserUid = os.Getenv("SUDO_UID")
  strSudoUserGid = os.Getenv("SUDO_GID")
 } else {
  fmt.Println("Please use sudo to run application (sudo ./port_listen)")
  return 4
 }
 strWorkDir, err = os.Getwd()
 if err != nil {
  fmt.Println("Error getting working directory: " + err.Error())
 }
 strWorkDir = strWorkDir + "/port_listen_logs"
 fiDirectory, err = os.Stat(strWorkDir)
 if err != nil {
  fmt.Println("Logs directory does not exist. Creating directory " + strWorkDir)
  err = os.Mkdir(strWorkDir, 0700)
  if err != nil {
   fmt.Println("Error creating directory; " + err.Error())
   return 5
  }
  intSudoUserUid, _ = strconv.Atoi(strSudoUserUid)
  intSudoUserGid, _ = strconv.Atoi(strSudoUserGid)
  err = os.Chown(strWorkDir, intSudoUserUid, intSudoUserGid)
  if err != nil {
   fmt.Println("Error changing permissions on directory: " + err.Error())
   return 7
  }
 }
 fiDirectory, err = os.Stat(strWorkDir)
 if !fiDirectory.IsDir() {
  fmt.Println(strWorkDir + " is not a directory!")
  return 6
 }

 command := exec.Command("/bin/launchctl", "limit", "maxfiles", "1000000", "1000000")
 _, err = command.Output()
 if err != nil {
  return 1
 }
 err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
 if err != nil {
  return 3
 }
 rLimit.Cur = 1400
 err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
 if err != nil {
  return 2
 }
 return 0
}

Here ends part 5! But I'm not done yet...

No comments:

Post a Comment