Proxying a Legacy MUD

In today’s world, it makes sense to allow playing a MUD directly from a web browser. But when you are faced with a legacy MUD that hasn’t been updated since the turn of the millenium, it can be difficult to update it sufficiently to support websockets.

Instead, this post will discuss using a simple proxy service written in Go which will handle the conversion between websockets and the MUD’s innate telnet capabilities.

Try it out

This is currently being used to proxy Krishnak mud.

Try it out!

Setting Up

Create a new Go project. For this post, I’ll be assuming your project will live in $GOPATH/src/krishnak.org/krishproxy.

Create a new main.go file with these contents:

package main

import "krishnak.org/krishproxy/server"

func main() {
	gameServer := server.NewGameServer()
	gameServer.Start("", 4500)
}

Then create a new package called server and create a new file in the package called server.go with these contents:

package server

import (
	log "github.com/Sirupsen/logrus"
	"github.com/gorilla/websocket"
	"github.com/labstack/echo"
	"github.com/labstack/echo/engine/standard"
	"github.com/labstack/echo/middleware"
	"net"
	"net/http"
	"strconv"
	"fmt"
	"encoding/base64"
	"time"
)

const (
	maxMessageSize = 4096
	pongWait = 30 * time.Second
)

type (
	GameServer interface {
		Start(host string, port int)
		Shutdown()
	}

	webserver struct {
	}

)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

func NewGameServer() GameServer {
	return &webserver{}
}

func isASCII(s string) bool {
	for _, c := range s {
		if c > 127 {
			return false
		}
	}
	return true
}

func (s *webserver) wsHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		c, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			log.Print("upgrade:", err)
			return
		}
		defer c.Close()

		c.SetReadLimit(maxMessageSize)
		c.SetReadDeadline(time.Now().Add(pongWait))
		c.SetPongHandler(func(string) error { c.SetReadDeadline(time.Now().Add(pongWait)); return nil })

		err = c.WriteMessage(websocket.TextMessage, []byte("Connecting to game..."))
		if err != nil {
			return
		}

		conn, err := net.Dial("tcp", "127.0.0.1:4000")
		if err != nil {
			fmt.Println(err)
			err = c.WriteMessage(websocket.TextMessage, []byte("Sorry! The game is currently unavailable. Please try again later!"))
			return
		}

		//connbuf := bufio.NewReader(conn)
		closech := make(chan struct{})
		mudSend := make(chan string)
		ticker := time.NewTicker(1500 * time.Millisecond)
		defer func() {
			ticker.Stop()
		}()
		go func() {
			p := make([]byte, 4096)
			for {
				n, err := conn.Read(p)
				if err != nil {
					fmt.Println("Closing socket due to error")
					closech <- struct{}{}
					return
				}
				str := string(p[:n])
				data := []byte(str)
				str = base64.StdEncoding.EncodeToString(data)
				mudSend <- str
			}
		}()
		go func() {
			for {
				// Read
				_, msg, err := c.ReadMessage()
				if err != nil {
					log.Infof("WS closing")
					closech <- struct{}{}
					return
				}
				if len(msg) < 4096 && isASCII(string(msg)) {
					conn.Write(msg)
				} else {
					closech <- struct{}{}
					return
				}
			}
		}()
		for {
			select {
			case <- closech:
				conn.Close()
				return
			case str := <- mudSend:
				err = c.WriteMessage(websocket.TextMessage, []byte(str))
				if err != nil {
					fmt.Println("Bad input.")
					closech <- struct{}{}
					return
				}
			case <-ticker.C:
				if err := c.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
					fmt.Println("Ping failed.")
					closech <- struct{}{}
					return
				}
				//conn.Write([]byte("\r\n"))
			}
		}
	}
}

func (s *webserver) Start(host string, port int) {
	addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
	if err != nil {
		log.Errorf("Error getting host:port address: %s", err)
		return
	}

	e := echo.New()
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.Static("./public"))
	e.GET("/ws", standard.WrapHandler(http.HandlerFunc(s.wsHandler())))
	e.Run(standard.New(addr.String()))
}

func (s *webserver) Shutdown() {

}

Now go get all of the packages.

Some Explanation

So far, we’ve created a simple Go proxy using the echo http framework and the gorilla websocket websocket package.

The server will listen by default on port 4500 and will connect to 127.0.0.1 (localhost) port 4000. If your MUD is running on a different port, you’ll need to update the port in the wsHandler function.

On port 4500, the Go server will serve up files in the public directory, so next we’ll need to set up the game play page.

So in the root of your Go package, add a new directory called public and inside that directory create a new file called index.html with these contents:

<!doctype html>
<head>
    <link rel="stylesheet" href="css/xterm.css"/>
    <style>
        html,body {height:100%;}
        body {margin:0;height:100%;}
        #terminal-container {height:calc(100% - 80px);width:100%;background-color:black;}
    </style>
</head>
<body>
<h1>Krishnak MUD</h1>
<div id="terminal-container"></div>
<script src="js/xterm.js"></script>
<script src="js/fit.js"></script>
<script>
    var terminalContainer = document.getElementById('terminal-container'),
            term = new Terminal(),
            socket,
            buffer = ''
            echo = true;
    term.open(terminalContainer);
    term.fit();
    term.setCursorStyle(3);

    socket = new WebSocket("ws://localhost:4500/ws");
    socket.onclose = function(evt) {
        term.write("Connection closed... Reload page to reconnect.\n")
    };
    socket.onmessage = function (evt) {
        try {
            var decodedString = atob(evt.data);
            if (decodedString.indexOf('ÿü') != -1) {
                echo = true;
                decodedString = decodedString.replace("ÿü", "");
            }
            if (decodedString.indexOf('ÿû') != -1) {
                echo = false;
                decodedString = decodedString.replace("ÿû", "");
            }
            term.write(decodedString);
        } catch(e) {
            console.log(evt.data);
        }
    };
    socket.onopen = function(evt) {
        term.write("Connected to proxy...");
    };
    term.on('data', function(data) {
        console.log(data, data.charCodeAt(0));
        if (data === '\n' || data === '\r') {
            socket.send(buffer + '\r\n');
            buffer = '';
            term.write("\r\n");
        } else if (data.charCodeAt(0) !== 127) {
            buffer += data;
            if (echo) {
                term.write(data);
            }
        } else {
            if (buffer.length > 0) {
                buffer = buffer.slice(0, -1);
                term.write("\x08 \x08");
            }
        }
    });
</script>
</body>

This is the base page which will set up the terminal emulation environment and connect to the Go proxy server. We’ll use the xterm.js terminal emulation library to handle the display of our MUD.

To store the javascript and CSS files go ahead and create a js directory and a css directory in the public directory.

You’ll need to download the xterm.js code from GitHub and build it, then copy the xterm.js and fit.js files to the public/js directory and the xterm.css file to the public/css directory.

All set? Great!

Compile the Proxy

Now we can build the proxy for testing by going back to the Go package root and using go build which should build a new krishproxy executable.

You should now be able to type ./krishproxy and then open the index.html page you created by visiting http://localhost:4500 in your browser.

If all went well, you should see something like: Krishnak Proxy Example

Go ahead and try logging into your Legacy MUD. You should be able to enter the game and perform the basics.

Next Steps

The code supplied above was a quick hack written in about 45 minutes and is just barely sufficient to enable logging into a legacy MUD from a web browser. It “supports” the telnet echo on and echo off sequences and thanks to the xterm.js terminal emulation, ANSI colors and positioning are supported.

If your game does anything beyond those basics, such as using MXP, GMCP or other MUD protocols, you’ll need to extend the javascript code to handle the sequences that you need in the fashion most appropriate to your MUD.

I’d appreciate any pull requests or bug reports if you do use this code.

Download

To make it easier to get started, I’ve uploaded the code to GitHub, so visit the repository page and clone or download a release like you’d normally do.

Please don’t hesitate to ask a question in the comments below if you have any problems or feedback.

Thanks!

–Untermina