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.
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:
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