💾 Archived View for blinkyshark.chickenkiller.com › 2022-06-03-sss.gmi captured on 2022-06-03 at 22:56:52. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

sss: simple spartan server

Created 2022-06-03

I wanted to play around with the Spartan protocol. To that end, I bodged together a simple server written in Go.

It weighs in at 114 lines. It isn't complete, or pretty. Here's the code as it stands at 3 Jun 2002:

/* simple server for the spartan protocol
 */

package main

import (
	"bufio"
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"
)

const (
	CONN_HOST = "localhost"
	//CONN_HOST = "0.0.0.0"
	//CONN_HOST = ""
	//CONN_HOST = "127.0.0.1"
	//CONN_HOST = "192.168.0.27"
	//CONN_HOST = "blinkyshark.chickenkiller.com"
	CONN_PORT = "3000"
	CONN_TYPE = "tcp"
	DATA_DIR  = "/home/pi/repos/cerbo/website/gemini"
)

func main() {
	// Listen for incoming connections.
	l, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
	if err != nil {
		fmt.Println("Error listening:", err.Error())
		os.Exit(1)
	}
	// Close the listener when the application closes.
	defer l.Close()
	fmt.Println("Listening on " + CONN_HOST + ":" + CONN_PORT)
	for {
		// Listen for an incoming connection.
		conn, err := l.Accept()
		if err != nil {
			fmt.Println("Error accepting: ", err.Error())
			os.Exit(1)
		}
		// Handle connections in a new goroutine.
		go handleRequest(conn)
	}
}

func get_path(conn net.Conn) (string, error) {

	oops := func(msg string) (string, error) { return "", errors.New(msg) }

	buf := make([]byte, 1024) // buffer to hold incoming data
	reqLen, err := conn.Read(buf) // Read the incoming connection into the buffer.
	//fmt.Println("reqLen:", reqLen)
	if err != nil { return "", err }
	fields := strings.Fields(string(buf[0:reqLen]))
	if len(fields) != 3 { return oops("bad header. Expected 3 field") }

	path_in := fields[1]
	if path_in == "/" { path_in = "index.gmi" }
	path, err := filepath.Abs(DATA_DIR + "/" + path_in)
	if err != nil { return "", err }

	fmt.Println("path requested:", path)

	// check for out-of-directory stuff
	if len(path) < len(DATA_DIR) { return oops("path name is too short") }
	if DATA_DIR != path[0:len(DATA_DIR)] { return oops("data root voilated") }

	info, err := os.Stat(path)
	if err != nil { return "", err }
	if info.IsDir() { return oops("Won't serve directories") }

	fmt.Println("server=", fields[0])
	fmt.Println("loc=", path)
	fmt.Println("Received request ", string(buf[0:reqLen]))
	return path, err
}

// Handles incoming requests.
func handleRequest(conn net.Conn) {
	defer conn.Close()

	oops := func(err_str string) {
		err_str = "4 " + err_str;
		fmt.Println(err_str)
		conn.Write([]byte(err_str + "\r\n"))
	}

	pathname, err := get_path(conn)
	if err != nil { oops(err.Error()) ; return }
	
	fp, err := os.Open(pathname)
	if err != nil { oops("file not found") ; return }
	defer fp.Close()

	response := "2 text/gemini"
	if len(pathname) < 4 || pathname[len(pathname)-4:] != ".gmi" {
		response = "2 text/plain; charset=utf-8"
	}
	conn.Write([]byte(response + "\r\n"))

	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		txt1 := scanner.Text()
		//fmt.Println([]byte(txt1))
		txt1 += "\r\n"
		conn.Write([]byte(txt1))
	}

	if err := scanner.Err(); err != nil { oops(err.Error()) }
}

No doubt that there are security improvements to be made. Just tweaks the constants at the top of the file to suit your tastes. I use the FreeBSD packet filter (pf) to forward from port 300 to 3000.

I haven't set up a server permanently, so I've not got anything for you to poke around with.

My plan is that it will also server gopher content. Gemini content, too, if I can figure out how to get server-side TLS working.

I also have in mind the idea of using groff server-side. That way I'd be able to do stuff like have text that is short, but is reformaated nicely for the different protocols. Automatically-aligned text on gopher would be a neat feature, for example.

That's it for now.

See also

source code on GitLab

spartan protocol