Locate Your Laptop With Go Lang Part 2


programming
guide go

Refactor

You have seen how easy it is too make a server-client program in go that communicates within the same computer. Next, let us make the code more readable and streamlined. Sending back data from server to client isn’t really useful when we are building a program to locate a ‘missing’ laptop. Remove func reader(r io.Reader) function and go reader(c).

Also, variable incoming is more descriptive than just fd in server.go. We also rename echoServer to what_is_the_ip instead.

const (
	CONN_TYPE = "unix"
	CONN_PORT = "/tmp/echo.sock"
)

...

l, err := net.Listen(CONN_TYPE, CONN_PORT)
Next, we want to factor out magic strings in our code. We focus on net.Listen("unix", "/tmp/echo.sock"). Put these two parameters into constants.

Flags

It is possible to combine the server.go and client.go into a single file. We can take command line arguments passed into our compiled program. Within the main() function, we run off correspond function based to the argument being passed on.

func main() {
	flag.Parse()
	if mode == "server" || mode == "s" {
		runServer()
		return
	} else if mode == "client" || mode == "c" {
		runClient()
		return
	} else {
		fmt.Println("No recognisable flag")
	}
}

Our main() function has become simplified. It will only run appropriate function based on the argument be give to the program. Let’s take a look at how to parse command lin arguments.

var (
	mode		string
	serverAddr	string
	serverPort	int = 8088
	pollMinutes	int = 10  // 10 minutes

	response *http.Response
	body     []byte
)


func init() {
	flag.StringVar(&mode, "m", "server", "Mode: -m server|client")
	flag.StringVar(&serverAddr, "a", "0.0.0.0:8088", "Server address: -a 127.0.0.1")
	flag.IntVar(&serverPort, "p", serverPort, "Server port: -p 8088")
	flag.IntVar(&pollMinutes, "t", pollMinutes, "Poll interval (minutes): -t 10")
}

Notice that I put the code inside init() function. This is a special go way that will run before main(). It is sorts of initialization part of the program. - StringVar tells go that it expects a string. - &mode is the address of the variable that will store user’s input. - "m" is the name of the flag for mode of operation. - "server" is the default parameter is the user do not pass in any. - Lastly, "Mode: -m server|client" shows the help usage when user runs ./main --help in the terminal.

All of these are generated by go itself which is very convenient.

I also define our global variables. While global variable is bad in general, it is okay to use it here for commonly used variables.

Timestamp

While it is nice to see external IP being sent to the server, it would be nicer if we have a timestamp printed on the console as well. Let’s modify the code:

	for {
		buf := make([]byte, 512)
		nr, err := c.Read(buf)
		if err != nil {
			return
		}

		ip := buf[0:nr]
		time := time.Now().Format(time.RFC850)
		data := time + " " + string(ip)
		println("Server got:", string(data))
	}

We use a built in library, time to get current time in RFC850 format. A number of other formats are available from https://golang.org/pkg/time/. We store the current time inside time variable and append ip address received from client. We also need to import the time library.

import (
	"log"
	"net"
	"time"
)

Ten seconds is too often. Let’s change it to every ten minutes instead.

time.Sleep(time.Duration(pollMinutes) * time.Minute)

IP Lookup

So far, we have been only sending local ip address. This isn’t half useful if we want to find our missing laptop across the world. We need to be able to send client’s external IP address to the client. One easy way is to use a publicly available api.

func GetExternalIP() string {
	response, err := http.Get("http://ipv4bot.whatismyipaddress.com")
	if err != nil {
		log.Fatal("404 not found", err.Error())
		os.Exit(1)
	}
	defer response.Body.Close()

	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Fatal("http read error", err.Error())
		os.Exit(1)
	}
	src := string(body)

	return src
}
[]whatismyipaddress.com](whatismyipaddress.com) provides an api to retrieve external IP address. Using http.Get, we use a normal http GET request to the api and store its result into response variable. We read the whole thing with ReadAll and put it into a string as src.

File Log

We want to have a log file that records all incoming data from client. There are many ways of data persistence. Simplest is just store them in a text file.

func appendToLog(src string) {
	createFile(filename)
	file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
	if err != nil {

		panic(err.Error())
	}
	defer file.Close()

	if _, err = file.WriteString(src + "\n"); err != nil {
		panic(err.Error())
	}
}

func createFile(fileName string) {
	if _, err := os.Stat(fileName); os.IsNotExist(err) {
		newFile, err = os.Create(fileName)
		if err != nil {
			log.Fatal(err)
		}
		newFile.Close()
	}
}

First, we create a text file in the current directory with createFile(fileName string) function. Since we want to be as restrictive as possible, we create the text file with 0600 octal permission. Wel also make sure to append, not to overwrite the file to preserve all existing data.

Run them

Build the file (this time it is just single file!) go build main.go.

To run as a server, just type ./main. For a client, type ./main -m client. The program will use port 8088 by default if no port is specified.

Watch log.txt being filled with date, internal, and external ip address.

Friday, 11-Nov-16 09:46:13 AEDT 
	External IP: xxx.xxx.xxx.xxx
	Internal ip: 192.168.2.3

Full code: main.go

import (
	"fmt"
	"log"
	"net"
	"os"
	"time"
	"flag"
	"strings"
	"net/http"
	"io/ioutil"
	"strconv"
)

const (
	CONN_TYPE = "tcp"
	CONN_PORT = ":8088" // any port >= 1024
)

var (
	newFile		*os.File
	filename	string = "log.txt"
	mode		string
	serverAddr	string
	serverPort	int = 8088
	pollMinutes	int = 10  // 10 minutes

	err      error
	response *http.Response
	body     []byte
)


/*
======
Client
======
 */
func runClient() {
	fmt.Println("Running on client mode.")
	fmt.Println("Connecting to " + serverAddr)

	c, err := net.Dial("tcp", serverAddr)
	if err != nil {
		panic(err)
	}
	defer c.Close()
	for {
		var internalIP string = GetInternalIP()
		var externalIP string = GetExternalIP()

		time.Sleep(time.Duration(pollMinutes) * time.Minute)
	}
}

func GetInternalIP() string {
	conn, err := net.Dial("udp", "8.8.8.8:80")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	localAddr := conn.LocalAddr().String()
	idx := strings.LastIndex(localAddr, ":")

	return localAddr[0:idx]
}

func GetExternalIP() string {
	response, err := http.Get("http://ipv4bot.whatismyipaddress.com")
	if err != nil {
		log.Fatal("404 not found", err.Error())
		os.Exit(1)
	}
	defer response.Body.Close()

	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Fatal("http read error", err.Error())
		os.Exit(1)
	}
	src := string(body)

	return src
}

/*
======
Server
======
 */
func runServer() {
	fmt.Println("Running on server mode")

	listen, err := net.Listen(CONN_TYPE, CONN_PORT)
	if err != nil {
		log.Fatal("listen error:", err.Error())
	}
	defer listen.Close()

	currTime := time.Now().Format(time.RFC850)
	fmt.Println(currTime + ": Listening to incoming connections...")

	for {
		incoming, err := listen.Accept()
		if err != nil {
			log.Fatal("accept error:", err.Error())
		}

		go getIPFromClient(incoming)
	}
}

func getIPFromClient(conn net.Conn) {
	for {
		buf := make([]byte, 512)
		length, err := conn.Read(buf)
		if err != nil {
			log.Fatal("Error connecting" + err.Error())
			os.Exit(1)
		}

		ip := buf[0:length]
		currTime := time.Now().Format(time.RFC850)
		data := currTime + " " + string(ip)
		println("Server got:", string(data))
		appendToLog(data)  // Write incoming message into a log file.
	}
	conn.Close()
}

/*
=========
File IO
=========
 */
func appendToLog(src string) {
	createFile(filename)
	file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
	if err != nil {

		panic(err.Error())
	}
	defer file.Close()

	if _, err = file.WriteString(src + "\n"); err != nil {
		panic(err.Error())
	}
}

func createFile(fileName string) {
	if _, err := os.Stat(fileName); os.IsNotExist(err) {
		newFile, err = os.Create(fileName)
		if err != nil {
			log.Fatal(err)
		}
		newFile.Close()
	}
}

/*
===============
Initialisation
===============
 */
func init() {
	flag.StringVar(&mode, "m", "server", "Mode: -m server|client")
	flag.StringVar(&serverAddr, "a", "0.0.0.0:8088", "Server address: -a 127.0.0.1")
	flag.IntVar(&serverPort, "p", serverPort, "Server port: -p 8088")
	flag.IntVar(&pollMinutes, "t", pollMinutes, "Poll interval (minutes): -t 10")
}

/*
============
Entry
============
 */
func main() {
	flag.Parse()
	if mode == "server" || mode == "s" {
		runServer()
		return
	} else if mode == "client" || mode == "c" {
		runClient()
		return
	} else {
		fmt.Println("No recognisable flag")
	}
}
comments powered by Disqus