I’ve been listening to more Brad & Will Made a Tech Pod and the current episode triggered a bunch of nostalgia about using finger to work out what my fellow CS students at university were up to. I won’t go into to too much detail about what Finger is as the podcast covers it all.
This podcast has triggered things like this in the past, like when I decided to make this blog (and Brad & Will’s podcast) available via Gopher.
On the podcast they had Ben Brown as a guest who had written his own Finger Daemon and linked it up to a site called Happy Net Box where users can update their plan file. Then anybody can access it using the finger command e.g. finger hardillb@happynetbox.com
. The finger command is shipped by default on Windows, OSx and Linux so can be accessed from nearly anywhere.
I really liked the idea of resurrecting finger and as well as having a play with Happy Net Box I decided to see if I could run my own.
I started to look at what it would take to run a finger daemon on one of my Raspberry Pis, but while there are 2 packaged they don’t appear to run on current releases as they rely on init.d rather than Systemd.
Next up I thought I’d have a look at the protocol, which is documented in RFC1288. It is incredibly basic, you just listen on port 79 and read the username terminated with a new line & carriage return. This seamed to be simple enough to implement so I thought I’d give it a try in Go (and I needed something to do while all tonight’s TV was taken up with 22 men chasing a ball round a field).
The code is on Github here.
package main
import (
"io"
"os"
"fmt"
"net"
"path"
"time"
"strings"
)
const (
CONN_HOST = "0.0.0.0"
CONN_PORT = "79"
CONN_TYPE = "tcp"
)
func main () {
l, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
if err != nil {
fmt.Println("Error opening port: ", err.Error())
os.Exit(1)
}
defer l.Close()
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting connection: ", err.Error())
continue
}
go handleRequest(conn)
}
}
func handleRequest(conn net.Conn) {
defer conn.Close()
currentTime := time.Now()
buf := make([]byte, 1024)
reqLen, err := conn.Read(buf)
fmt.Println(currentTime.Format(time.RFC3339))
if err != nil {
fmt.Println("Error reading from: ", err.Error())
} else {
fmt.Println("Connection from: ", conn.RemoteAddr())
}
request := strings.TrimSpace(string(buf[:reqLen]))
fmt.Println(request)
parts := strings.Split(request, " ")
wide := false
user := parts[0]
if parts[0] == "/W" && len(parts) == 2 {
wide = true
user = parts[1]
} else if parts[0] == "/W" && len(parts) == 1 {
conn.Write([]byte("\r\n"))
return
}
if strings.Index(user, "@") != -1 {
fmt.Println("remote")
conn.Write([]byte("Forwarding not supported\r\n"))
} else {
if wide {
//TODO
} else {
pwd, err := os.Getwd()
filePath := path.Join(pwd, "plans", path.Base(user + ".plan"))
filePath = path.Clean(filePath)
fmt.Println(filePath)
file, err := os.Open(filePath)
if err != nil {
//not found
// io.Write([]byte("Not Found\r\n"))
} else {
defer file.Close()
io.Copy(conn,file)
conn.Write([]byte("\r\n"))
}
}
}
}
Rather than deal with the nasty security problems with pulling .plan
files out of peoples home directories it uses a directory called plans
and loads files that match the pattern <username>.plan
I’ve also built it in a Docker container and mounted a local directory to allow me to edit and add new plan files.
You can test it with finger ben@hardill.me.uk