Browse Source

Moving to golang for better performance. Also, channels and goroutines are really better than fibers.

Beoran 8 years ago
parent
commit
56746e469e

+ 1 - 0
go/bin

@@ -0,0 +1 @@
+../bin/

+ 1 - 0
go/pkg

@@ -0,0 +1 @@
+../pkg/

+ 0 - 0
src/callrb.c → oldsrc/callrb.c


+ 0 - 0
src/client.c → oldsrc/client.c


+ 0 - 0
src/config.c → oldsrc/config.c


+ 0 - 0
src/dynar.c → oldsrc/dynar.c


+ 0 - 0
src/esh.c → oldsrc/esh.c


+ 0 - 0
src/every.c → oldsrc/every.c


+ 0 - 0
src/libtelnet.c → oldsrc/libtelnet.c


+ 0 - 0
src/main.c → oldsrc/main.c


+ 0 - 0
src/mem.c → oldsrc/mem.c


+ 0 - 0
src/monolog.c → oldsrc/monolog.c


+ 0 - 0
src/rh.c → oldsrc/rh.c


+ 0 - 0
src/server.c → oldsrc/server.c


+ 0 - 0
src/timer.c → oldsrc/timer.c


+ 0 - 0
src/toruby.c → oldsrc/toruby.c


+ 0 - 0
src/tr_file.c → oldsrc/tr_file.c


+ 227 - 0
src/woe/monolog/monolog.go

@@ -0,0 +1,227 @@
+
+/* A package that provides advanced logging facilities. */
+package monolog
+
+import (
+    "os"
+    "fmt"
+    "time"
+    "runtime"
+    "path/filepath"
+)
+
+  
+type Logger interface {
+    Log(level string, file string, line int, format string, args...interface{})
+    Close()
+}
+
+
+func (me * FileLogger) WriteLog(depth int, level string, format string, args ... interface{}) {
+    _ ,file, line, ok := runtime.Caller(depth)
+    
+    if !ok {
+        file = "unknown"
+        line = 0
+    }
+    
+    me.Log(level, file, line, format, args...)
+}
+
+func (me * FileLogger) NamedLog(name string, format string, args ... interface{}) {
+    me.WriteLog(2, name, format, args...)
+}
+
+func (me * FileLogger) Info(format string, args ... interface{}) {
+    me.WriteLog(2, "INFO", format, args...)
+}
+
+func (me * FileLogger) Warning(format string, args ... interface{}) {
+    me.WriteLog(2, "WARNING", format, args...)
+}
+
+func (me * FileLogger) Error(format string, args ... interface{}) {
+    me.WriteLog(2, "ERROR", format, args...)
+}
+
+func (me * FileLogger) Fatal(format string, args ... interface{}) {
+    me.WriteLog(2, "FATAL", format, args...)
+}
+
+func (me * FileLogger) Debug(format string, args ... interface{}) {
+    me.WriteLog(2, "DEBUG", format, args...)
+}
+
+  
+type FileLogger struct {
+    filename      string
+    file        * os.File
+}
+
+func (me * FileLogger) Close() {
+    if (me.file != nil) { 
+        me.file.Close()
+    }
+    me.file = nil
+}
+
+func (me * FileLogger) Log(level string, file string, line int, format string, args...interface{}) {
+    fileshort := filepath.Base(file)
+    now := time.Now().Format(time.RFC3339)
+    fmt.Fprintf(me.file, "%s: %s: %s: %d: ", now, level, fileshort, line)
+    if args != nil && len(args) > 0 { 
+        fmt.Fprintf(me.file, format, args...)
+    } else {
+        fmt.Fprint(me.file, format)
+    }
+    fmt.Fprint(me.file, "\n")
+}
+
+
+func NewFileLogger(filename string) (logger Logger, err error) {
+    file, err := os.OpenFile(filename, os.O_WRONLY | os.O_APPEND | os.O_CREATE, 0660) 
+    
+    if err != nil { 
+        return nil, err
+    }
+    
+    return &FileLogger{filename, file}, nil
+}    
+
+func NewStderrLogger() (logger Logger, err error) {    
+    return &FileLogger{"/dev/stderr", os.Stderr}, nil
+}
+
+
+func NewStdoutLogger() (logger Logger, err error) {    
+    return &FileLogger{"/dev/stderr", os.Stdout}, nil
+}
+
+type Log struct {
+    loggers          [] Logger
+    levels  map[string] bool
+}
+
+
+func NewLog() * Log {
+    loggers :=  make([] Logger, 32)
+    levels  :=  make(map[string] bool)
+    return &Log{loggers, levels}
+}
+
+func (me * Log) AddLogger(logger Logger) {
+    me.loggers = append(me.loggers, logger)
+}
+
+func (me * Log) EnableLevel(level string) {
+    me.levels[level] = true
+}
+
+func (me * Log) DisableLevel(level string) {
+    me.levels[level] = false
+}
+
+func (me * Log) LogVa(name string, file string, line int, format string, args...interface{}) {
+    _, ok := me.levels[name]
+    
+    if !ok {
+        return
+    }
+    
+    for _ , logger := range me.loggers {
+        if (logger != nil) {
+            logger.Log(name, file, line, format, args...)
+        }
+    }
+}
+
+func (me * Log) Close() {    
+    for index , logger := range me.loggers {
+        if logger != nil {
+            logger.Close()
+            me.loggers[index] = nil
+        }
+    }
+}
+
+var DefaultLog * Log
+
+func init() {
+    DefaultLog = NewLog()
+    // runtime.SetFinalizer(DefaultLog, DefaultLog.Close)
+}
+
+func EnableLevel(level string) {
+    DefaultLog.EnableLevel(level)
+}
+
+func DisableLevel(level string) {
+    DefaultLog.DisableLevel(level)
+}
+
+func AddLogger(logger Logger, err error) {
+    if err == nil {
+        DefaultLog.AddLogger(logger)
+    }
+}
+
+
+func Setup(name string, stderr bool, stdout bool) {    
+    if name != "" {
+        AddLogger(NewFileLogger(name))
+    }
+    
+    if stderr { 
+        AddLogger(NewStderrLogger())
+    }
+    
+    if stdout { 
+        AddLogger(NewStdoutLogger())
+    }
+    
+    EnableLevel("INFO")
+    EnableLevel("WARNING")
+    EnableLevel("ERROR")
+    EnableLevel("FATAL")    
+}     
+
+func Close() {
+    DefaultLog.Close()
+}
+
+
+func WriteLog(depth int, name string, format string, args ... interface{}) {
+    _ ,file, line, ok := runtime.Caller(depth)
+    if !ok {
+        file = "unknown"
+        line = 0
+    }    
+    DefaultLog.LogVa(name, file, line, format, args...)
+}
+
+func NamedLog(name string, format string, args ...interface{}) {
+    WriteLog(2, name, format, args)
+}
+
+func Info(format string, args ...interface{}) {
+    WriteLog(2, "INFO", format, args...)
+}
+
+func Warning(format string, args ...interface{}) {
+    WriteLog(2, "WARNING", format, args...)
+}
+
+func Error(format string, args ...interface{}) {
+    WriteLog(2, "ERROR", format, args...)
+}
+
+func Fatal(format string, args ...interface{}) {
+    WriteLog(2, "FATAL", format, args...)
+}
+
+func Debug(format string, args ...interface{}) {
+    WriteLog(2, "DEBUG", format, args...)
+}
+
+
+

+ 91 - 0
src/woe/server/client.go

@@ -0,0 +1,91 @@
+package server
+
+
+import (
+    // "fmt"
+    "net"
+    "time"
+    // "errors"
+    // "io"
+    "github.com/beoran/woe/monolog"
+)
+
+type Client struct {
+    server * Server
+    id       int
+    conn     net.Conn
+    telnet   interface{}
+    alive    bool
+    timeout  int
+    datachan chan []byte
+    errchan  chan error
+    timechan chan time.Time 
+}
+
+
+func NewClient(server * Server, id int, conn net.Conn) * Client {
+    datachan := make (chan []byte, 1024)
+    errchan  := make (chan error, 1)
+    timechan := make (chan time.Time, 32)
+    return &Client{server, id, conn, nil, true, -1, datachan, errchan, timechan}
+}
+
+func (me * Client) Close() {
+    me.conn.Close()
+    me.alive = false
+}
+
+/** Goroutine that does the actual reading of input data, and sends it to the 
+ * needed channels. */    
+func (me * Client) ServeRead() {
+    for (me.alive) { 
+        buffer  := make([]byte, 1024, 1024)
+        _ , err := me.conn.Read(buffer);
+        if err != nil {
+            me.errchan <- err
+            return
+        }
+        me.datachan <- buffer
+    }
+}
+
+
+func (me * Client) TryRead(millis int) (data [] byte, timeout bool, close bool) {
+    select {
+        case data := <- me.datachan:
+            return data, false, false
+                       
+        case err  := <- me.errchan:
+            monolog.Info("Connection closed: %s\n", err)
+            me.Close()
+            return nil, false, true
+            
+        case _ = <- time.Tick(time.Millisecond * time.Duration(millis)):
+            return nil, true, false
+    }
+}
+
+
+func (me * Client) Serve() (err error) {
+    // buffer := make([]byte, 1024, 1024)
+    go me.ServeRead()
+    for (me.alive) {
+        data, _, _ := me.TryRead(3000)
+        
+        if data == nil {
+            me.conn.Write([]byte("Too late!\r\n"))
+        } else {
+            me.server.Broadcast(string(data))
+        }
+        
+    }
+    return nil
+}
+
+func (me * Client) IsAlive() bool {
+    return me.alive
+}
+
+func (me * Client) WriteString(str string) {
+    me.conn.Write([]byte(str))
+}

+ 232 - 0
src/woe/server/server.go

@@ -0,0 +1,232 @@
+package server
+
+import (
+    "log"
+  //  "io"
+    "net"
+  //  "errors"
+    "os"
+   "time"
+   "fmt"
+   "github.com/beoran/woe/monolog"
+)
+
+var MSSP map[string] string
+
+const MAX_CLIENTS = 1000
+
+func init() {
+     MSSP = map[string] string {
+          "NAME"        : "Workers Of Eruta",
+          "UPTIME"      : string(time.Now().Unix()),
+          "PLAYERS"     : "0",
+          "CRAWL DELAY" : "0",
+          "CODEBASE"    : "WOE",
+          "CONTACT"     : "beoran@gmail.com",
+          "CREATED"     : "2015",
+           "ICON"       : "None",
+          "LANGUAGE"    : "English",
+          "LOCATION"    : "USA",
+          "MINIMUM AGE" : "18",
+          "WEBSITE"     : "beoran.net",
+          "FAMILY"      : "Custom",
+          "GENRE"       : "Science Fiction",
+          "GAMEPLAY"    : "Adventure",
+          "STATUS"      : "Alpha",
+          "GAMESYSTEM"  : "Custom",
+          "INTERMUD"    : "",
+          "SUBGENRE"    : "None",
+          "AREAS"       : "0",
+          "HELPFILES"   : "0",
+          "MOBILES"     : "0",
+          "OBJECTS"     : "0",
+          "ROOMS"       : "1",
+          "CLASSES"     : "0",
+          "LEVELS"      : "0",
+          "RACES"       : "3",
+          "SKILLS"      : "900",
+          "ANSI"        : "1",
+          "MCCP"        : "1",
+          "MCP"         : "0",
+          "MSDP"        : "0",
+          "MSP"         : "0",
+          "MXP"         : "0",
+          "PUEBLO"      : "0",
+          "UTF-8"       : "1",
+          "VT100"       : "1",
+          "XTERM 255 COLORS" : "1",
+          "PAY TO PLAY"      : "0",
+          "PAY FOR PERKS"    : "0",
+          "HIRING BUILDERS"  : "0",
+          "HIRING CODERS"    : "0" }  
+}
+
+
+
+
+type Server struct {
+    address               string
+    listener              net.Listener
+    logger              * log.Logger
+    logfile             * os.File
+    clients map[int]    * Client 
+    tickers map[string] * Ticker
+    alive                 bool                
+}
+
+
+type Ticker struct {
+    * time.Ticker
+    Server        * Server
+    Name            string
+    Milliseconds    int
+    callback        func(me * Ticker, t time.Time) (stop bool)
+}
+
+
+
+func NewServer(address string) (server * Server, err error) {
+    listener, err := net.Listen("tcp", address);
+    if (err != nil) { 
+        return nil, err
+    }
+    
+    logfile, err := os.OpenFile("log.woe", os.O_WRONLY | os.O_APPEND | os.O_CREATE, 0660) 
+    if (err != nil) {
+        return nil, err
+    }
+    
+    logger := log.New(logfile, "woe", log.Llongfile | log.LstdFlags)
+    clients := make(map[int] * Client)
+    tickers := make(map[string] * Ticker)
+
+    server = &Server{address, listener, logger, logfile, clients, tickers, true}    
+    server.AddDefaultTickers()
+    
+    return server, nil
+}
+
+func (me * Server) Close() {
+    me.logfile.Close();
+}
+
+
+func NewTicker(server * Server, name string, milliseconds int, callback func (me * Ticker, t time.Time) bool) (* Ticker) {
+    ticker := time.NewTicker(time.Millisecond * time.Duration(milliseconds))
+    return &Ticker {ticker, server, name, milliseconds, callback}
+}
+
+
+func (me * Ticker) Run() {
+    OUTER: 
+    for me.Server.alive {
+        for tick := range me.C {
+            if (!me.callback(me, tick)) {
+                break OUTER;
+            }
+        }
+    }
+}
+
+
+func (me * Server) RemoveTicker(name string) {
+    ticker, have := me.tickers[name]
+    if (!have) {
+        return
+    }    
+    ticker.Stop()
+    delete(me.tickers, name)
+}
+
+func (me * Server) StopTicker(name string) {
+    ticker, have := me.tickers[name]
+    if (!have) {
+        return
+    }    
+    ticker.Stop();
+}
+
+
+
+func (me * Server) AddTicker(name string, milliseconds int, callback func (me * Ticker, t time.Time) bool) (* Ticker) {
+    _, have := me.tickers[name]
+    
+    if have {
+        me.RemoveTicker(name)
+    }
+        
+    ticker := NewTicker(me, name, milliseconds, callback)
+    me.tickers[name] = ticker
+    go ticker.Run();
+    
+    return ticker
+}
+
+
+func onWeatherTicker (me * Ticker, t time.Time) bool {
+    monolog.Info("Weather Ticker tick tock.")
+    return true
+}
+
+
+func (me * Server) AddDefaultTickers() {
+    me.AddTicker("weather", 10000, onWeatherTicker)    
+}
+
+func (me * Server) handleDisconnectedClients() {
+    for { 
+        time.Sleep(1)
+        for id, client := range me.clients {
+            if (!client.IsAlive()) {
+                monolog.Info("Client %d has disconnected.", client.id)
+                client.Close()
+                delete(me.clients, id);
+            }
+        }   
+    }
+}
+
+func (me * Server) findFreeID() (id int, err error) {
+    for id = 0 ; id < MAX_CLIENTS ; id++ {
+        client, have := me.clients[id]
+        if (!have) || (client == nil) {
+            return id, nil
+        }
+    }
+    return -1, fmt.Errorf("Too many clients!");
+}
+
+func (me * Server) onConnect(conn net.Conn) (err error) {
+    id, err := me.findFreeID()
+    if err != nil {
+        monolog.Info("Refusing connection because no ID is available for %s. ", conn.RemoteAddr().String())
+        conn.Close()
+        return nil
+    }
+    monolog.Info("New client connected from %s, id %d. ", conn.RemoteAddr().String(), id)
+    client := NewClient(me, id, conn)
+    me.clients[id] = client
+    return client.Serve()
+}
+
+func (me * Server) Serve() (err error) { 
+    go me.handleDisconnectedClients()
+    
+    for (me.alive) {
+        conn, err := me.listener.Accept()
+        if err != nil {
+            return err
+        }
+        go me.onConnect(conn)
+    }
+    return nil
+}
+
+
+func (me * Server) Broadcast(message string) {
+    for _, client := range me.clients {
+        if (client.IsAlive()) {
+            client.WriteString(message)
+        }
+    }       
+}

+ 125 - 0
src/woe/telnet/codes.go

@@ -0,0 +1,125 @@
+package telnet
+
+const ( 
+    // Telnet commands
+    TELNET_IAC = 255
+    TELNET_DONT = 254
+    TELNET_DO = 253
+    TELNET_WONT = 252
+    TELNET_WILL = 251
+    TELNET_SB = 250
+    TELNET_GA = 249
+    TELNET_EL = 248
+    TELNET_EC = 247
+    TELNET_AYT = 246
+    TELNET_AO = 245
+    TELNET_IP = 244
+    TELNET_BREAK = 243
+    TELNET_DM = 242
+    TELNET_NOP = 241
+    TELNET_SE = 240
+    TELNET_EOR = 239
+    TELNET_ABORT = 238
+    TELNET_SUSP = 237
+    TELNET_EOF = 236
+
+    // Telnet options.
+    TELNET_TELOPT_BINARY = 0
+    TELNET_TELOPT_ECHO = 1
+    TELNET_TELOPT_RCP = 2
+    TELNET_TELOPT_SGA = 3
+    TELNET_TELOPT_NAMS = 4
+    TELNET_TELOPT_STATUS = 5
+    TELNET_TELOPT_TM = 6
+    TELNET_TELOPT_RCTE = 7
+    TELNET_TELOPT_NAOL = 8
+    TELNET_TELOPT_NAOP = 9
+    TELNET_TELOPT_NAOCRD = 10
+    TELNET_TELOPT_NAOHTS = 11
+    TELNET_TELOPT_NAOHTD = 12
+    TELNET_TELOPT_NAOFFD = 13
+    TELNET_TELOPT_NAOVTS = 14
+    TELNET_TELOPT_NAOVTD = 15
+    TELNET_TELOPT_NAOLFD = 16
+    TELNET_TELOPT_XASCII = 17
+    TELNET_TELOPT_LOGOUT = 18
+    TELNET_TELOPT_BM = 19
+    TELNET_TELOPT_DET = 20
+    TELNET_TELOPT_SUPDUP = 21
+    TELNET_TELOPT_SUPDUPOUTPUT = 22
+    TELNET_TELOPT_SNDLOC = 23
+    TELNET_TELOPT_TTYPE = 24
+    TELNET_TELOPT_EOR = 25
+    TELNET_TELOPT_TUID = 26
+    TELNET_TELOPT_OUTMRK = 27
+    TELNET_TELOPT_TTYLOC = 28
+    TELNET_TELOPT_3270REGIME = 29
+    TELNET_TELOPT_X3PAD = 30
+    TELNET_TELOPT_NAWS = 31
+    TELNET_TELOPT_TSPEED = 32
+    TELNET_TELOPT_LFLOW = 33
+    TELNET_TELOPT_LINEMODE = 34
+    TELNET_TELOPT_XDISPLOC = 35
+    TELNET_TELOPT_ENVIRON = 36
+    TELNET_TELOPT_AUTHENTICATION = 37
+    TELNET_TELOPT_ENCRYPT = 38
+    TELNET_TELOPT_NEW_ENVIRON = 39
+    TELNET_TELOPT_MSDP = 69
+    TELNET_TELOPT_MSSP = 70
+    TELNET_TELOPT_COMPRESS = 85
+    TELNET_TELOPT_COMPRESS2 = 86
+    TELNET_TELOPT_MSP = 90
+    TELNET_TELOPT_MXP = 91
+    TELNET_TELOPT_MSP2 = 92
+    TELNET_TELOPT_MSP2_MUSIC = 0
+    TELNET_TELOPT_MSP2_SOUND = 1
+
+
+
+    TELNET_TELOPT_ZMP = 93
+    TELNET_TELOPT_EXOPL = 255
+
+    TELNET_TELOPT_MCCP2 = 86
+
+    // TERMINAL-TYPE codes. 
+    TELNET_TTYPE_IS = 0
+    TELNET_TTYPE_SEND = 1
+    
+    // MTTS standard codes
+    TELNET_MTTS_ANSI                = 1
+    TELNET_MTTS_VT100               = 2
+    TELNET_MTTS_UTF8                = 4
+    TELNET_MTTS_256_COLORS          = 8
+    TELNET_MTTS_MOUSE_TRACKING      = 16
+    TELNET_MTTS_OSC_COLOR_PALETTE   = 32
+    TELNET_MTTS_SCREEN_READER       = 64
+    TELNET_MTTS_PROXY               = 128
+    
+
+    // NEW-ENVIRON/ENVIRON codes. 
+    TELNET_ENVIRON_IS = 0
+    TELNET_ENVIRON_SEND = 1
+    TELNET_ENVIRON_INFO = 2
+    TELNET_ENVIRON_VAR = 0
+    TELNET_ENVIRON_VALUE = 1
+    TELNET_ENVIRON_ESC = 2
+    TELNET_ENVIRON_USERVAR = 3
+
+    // MSSP codes. 
+    TELNET_MSSP_VAR = 1
+    TELNET_MSSP_VAL = 2
+    
+    // MSDP values.
+    TELNET_MSDP_VAR         = 1
+    TELNET_MSDP_VAL         = 2
+    TELNET_MSDP_TABLE_OPEN  = 3
+    TELNET_MSDP_TABLE_CLOSE = 4
+    TELNET_MSDP_ARRAY_OPEN  = 5
+    TELNET_MSDP_ARRAY_CLOSE = 6
+    
+    // newline, cr and nul
+    TELNET_CR = 13
+    TELNET_NL = 10
+    TELNET_NUL = 0
+)
+

+ 0 - 0
src/woe/telnet/rfc1143.go


+ 653 - 0
src/woe/telnet/telnet.go

@@ -0,0 +1,653 @@
+package telnet
+
+import "bytes"
+import "compression/zlib"
+import "github.com/beoran/woe/monolog"
+
+// This Telnet struct implements a subset of the Telnet protocol.
+
+// Telnet states
+type TelnetState int
+
+const (
+    data_state TelnetState  = iota,
+    iac_state               = iota,
+    will_state              = iota,
+    wont_state              = iota,
+    do_state                = iota,
+    dont_state              = iota
+    sb_state                = iota,
+    sb_data_state           = iota,
+    sb_data_iac_state       = iota
+)
+
+
+// Telnet event types
+
+type Event interface {
+    isEvent()
+}
+
+type DataEvent struct {
+    Data [] byte
+}
+
+func (me DataEvent) isEvent() {}
+
+type NAWSEvent struct {
+    W   int
+    H   int
+}
+
+func (me NAWSEvent) isEvent() {}
+
+
+type TTypeEvent struct {
+    Telopt  byte
+    Name    string
+}
+
+func (me TTypeEvent) isEvent() {}
+
+
+type SubnegotioateEvent struct {
+    Telopt    byte
+    Buffer [] byte
+}
+
+func (me SubnegotiateEvent) isEvent() {}
+
+
+type IACEvent struct {
+    Telopt    byte
+}
+
+func (me IACEvent) isEvent() {}
+
+
+type CompressionEvent struct {
+    Compress  bool
+}
+
+func (me CompressionEvent) isEvent() {}
+
+
+type EnvironmentEvent struct {
+    Telopt    byte
+    Vars      map[string] string
+}
+
+func (me EnvironmentEvent) isEvent() {}
+
+
+type MSSPEvent struct {
+    Telopt    byte
+    Vars      map[string] string
+}
+
+func (me MSSPEvent) isEvent() {}
+
+
+type ZMPEvent struct {
+    Telopt    byte
+    Vars      map[string] string
+}
+
+func (me ZMPEvent) isEvent() {}
+
+
+type EventChannel chan[Event]
+
+
+type Telopt struct {
+    telopt byte
+    us     byte
+    him    byte
+}
+    
+type Telnet struct { 
+  Events            EventChannel  
+  telopts map[byte] Telopt 
+  state             TelnetState 
+  compress          bool
+  zwriter           Writer
+  zreader           Reader
+  buffer          []byte
+  sb_telopt         byte
+}
+
+func New() telnet * Telnet {
+    events     := make(EventChannel, 64)
+    telopts    := make (map[byte] Telopt)
+    state      := data_state
+    compress   := false
+    zwriter    := nil
+    zreader    := nil
+    buffer     := make([]byte, 1024, 1024)
+    sb_telopt  := 0
+    telnet      = &Telnet { events, telopts, state, compress, 
+        zwriter, zreader, buffer, sb_telopt
+    }
+    return telnet
+}
+
+// Starts compresssion
+func (me * Telnet) StartCompression() {
+    // var zwbuf  bytes.Buffer
+    // me.zwriter = zlib.NewWriter(&zwbuf);
+}
+  
+// Closes the telnet connection, send last compressed data if needed.
+func (me * Telnet) Close() { 
+    if me.compress  {
+        me.zwriter.Close()
+        me.zreader.Close()    
+    }
+}
+
+// Filters raw text, only compressing it if needed. 
+func (me * Telnet) FilterRaw(in []byte, out chan []byte) {
+    // XXX Handle compression here later
+    out <- in
+} 
+
+// Filters text, escaping IAC bytes. 
+func (me * Telnet) FilterRaw(in []byte, out chan []byte) {
+    buffer := make([]byte, len(in) * 2, len(in) * 2) 
+    outdex := 0
+    /* Double IAC characters to escape them. */
+    for index := 0; index < len(in) ; index++ {
+        now := in[index]
+        if now == TELNET_IAC {
+            buffer[outdex] = TELNET_IAC; 
+            outdex++;    
+        }
+        buffer[outdex] = TELNET_IAC;
+        outdex++;
+    }
+    out <- buffer
+} 
+
+// Send negotiation bytes
+func (me * Telnet) SendNegotiate(cmd byte, telopt byte, out chan []byte) {
+    buffer      := make([]byte, 3)
+    buffer[0]    = TELNET_IAC
+    buffer[1]    = cmd
+    buffer[2]    = telopt
+    me.FilterRaw(buffer, out)
+}    
+   
+// Parse a subnegotiation buffer for a naws event
+func (me * Telnet) SubnegotiateNAWS(buffer []byte,  )
+    // Some clients, like Gnome-Mud can't even get this right. Grrr!
+    // XXx continue here
+    if buffer.nil? || buffer.empty? || buffer.size != 4
+      monolog.Info("Bad NAWS negotiation: #{buffer}")
+      return nil
+    end
+    arr   = buffer.bytes.to_a
+    w     = (arr[0] << 8) + arr[1]
+    h     = (arr[2] << 8) + arr[3]
+    send_event(:naws, w, h)
+  end
+  
+
+  # Storage for environment values
+  class Environment 
+    attr_accessor :type
+    attr_accessor :value
+    
+    def initialize(type, value)
+      me.type   = type
+      me.value  = value
+    end
+  end
+
+
+  # process an ENVIRON/NEW-ENVIRON subnegotiation buffer
+  def subnegotiate_environ(buffer)
+    vars  = []
+    cmd   = ""
+    arr   = buffer.bytes.to_a
+    fb    = arr.first  
+    # first byte must be a valid command 
+    if fb != TELNET_ENVIRON_SEND && fb != TELNET_ENVIRON_IS && fb != TELNET_ENVIRON_INFO
+      log_error("telopt environment subneg command not valid")
+      return 0
+    end
+    
+    cmd << fb    
+    
+    if (buffer.size == 1) 
+      send_event(:environment, fb, vars)
+      return false
+    end
+        
+    # Second byte must be VAR or USERVAR, if present
+    sb = arr[1]
+    if sb != TELNET_ENVIRON_VAR && fb != TELNET_ENVIRON_USEVAR
+      log_error("telopt environment subneg missing variable type")
+      return false
+    end
+    
+    # ensure last byte is not an escape byte (makes parsing later easier) 
+    lb = arr.last
+    if lb == TELNET_ENVIRON_ESC
+      log_error("telopt environment subneg ends with ESC")
+      return false
+    end
+
+    var    = nil
+    index  = 1
+    escape = false
+    
+    arr.shift
+    
+    arr.each do | c | 
+      case c
+      when TELNET_ENVIRON_VAR
+      when TELNET_ENVIRON_VALUE
+      when TELNET_ENVIRON_USERVAR
+        if escape
+          escape = false
+          var.value << c
+        elsif var
+          vars << var
+          var = Environment.new(c, "")
+        else
+          var = Environment.new(c, "")        
+        end
+      when TELNET_ENVIRON_ESC
+        escape = true
+      else
+        var.value << c  
+      end # case
+    end # each
+    
+    send_event(:environment, fb, vars)    
+    return false
+  end
+
+
+
+# process an MSSP subnegotiation buffer
+def subnegotiate_mssp(buffer)
+  telnet_event_t ev;
+  struct telnet_environ_t *values;
+  char *var = 0;
+  char *c, *last, *out;
+  size_t i, count;
+  unsigned char next_type;
+  
+  if buffer.size < 1
+    return 0
+  end
+  
+  arr   = buffer.bytes.to_a
+  fb    = arr.first  
+  # first byte must be a valid command
+  if fb != TELNET_MSSSP_VAR
+    log_error("telopt MSSP subneg data not valid")
+    return false
+  end
+  
+  vars    = {}
+  var     = ""
+  val     = ""
+  mstate  = :var
+  while index <  arr.size
+    c     = arr[index]
+    case c
+    when TELNET_MSSP_VAR
+      mstate = :var
+      if mstate == :val
+        vars[var] = val
+        var = ""
+        val = ""
+      end      
+    when TELNET_MSSP_VAL
+      mstate = :val
+    else
+      if mstate == :var
+        var << c  
+      elsif mstate == :val
+        val << c  
+      end      
+    end # case
+    index += 1
+  end # while
+  
+  send_event(:mssp, vars)
+  return false
+end
+
+
+# parse ZMP command subnegotiation buffers 
+def subnegotiate_zmp(buffer)
+  args = []
+  arg  = ""
+  
+  buffer.each_byte do |b|  
+    if b == 0
+      args << arg
+      arg = ""
+    else
+      arg << byte
+    end
+  end
+  send_event(:zmp, vars)
+  return false
+end
+
+# parse TERMINAL-TYPE command subnegotiation buffers
+def subnegotiate_ttype(buffer)
+  # make sure request is not empty
+  if buffer.size == 0
+    log_error("Incomplete TERMINAL-TYPE request");
+    return 0
+  end
+  
+  arr   = buffer.bytes
+  fb    = arr.first
+  term  = nil 
+  
+  if fb == TELNET_TTYPE_IS
+    term = buffer[1, buffer.size]
+    send_event(:ttype_is, term)
+  elsif fb == TELNET_TTYPE_SEND
+    term = buffer[1, buffer.size]
+    send_event(:ttype_send, term)
+  else
+    log_error("TERMINAL-TYPE request has invalid type")
+    return false
+  end
+  return false
+end
+
+
+# process a subnegotiation buffer; returns true if the current buffer
+# must be aborted and reprocessed due to COMPRESS2 being activated
+
+def do_subnegotiate(buffer)
+  case me.sb_telopt
+  when TELNET_TELOPT_COMPRESS2
+    # received COMPRESS2 begin marker, setup our zlib box and
+    # start handling the compressed stream if it's not already.
+    me.compress = true
+    send_event(:compress, me.compress)
+    return true
+  # specially handled subnegotiation telopt types
+  when TELNET_TELOPT_ZMP
+    return subnegotiate_zmp(buffer)
+  when TELNET_TELOPT_TTYPE
+    return subnegotiate_ttype(buffer)
+  when TELNET_TELOPT_ENVIRON  
+    return subnegotiate_environ(buffer)
+  when TELNET_TELOPT_NEW_ENVIRON
+    return subnegotiate_environ(buffer)
+  when TELNET_TELOPT_MSSP
+    return subnegotiate_mssp(buffer)
+  when TELNET_TELOPT_NAWS
+    return subnegotiate_naws(buffer)
+  else
+    send_event(:subnegotiate, me.sb_telopt, buffer)
+    return false
+  end
+end
+
+
+  
+  def process_byte(byte) 
+    # p "process_byte, #{me.state} #{byte}"
+    case me.state
+    # regular data
+    when :data
+      if byte == TELNET_IAC
+        # receive buffered bytes as data and go to IAC state if it's notempty
+        send_event(:data, me.buffer) unless me.buffer.empty?
+        me.buffer = ""
+        me.state = :iac
+      else
+        me.buffer << byte
+      end
+    # IAC received before
+    when :iac
+      case byte
+      # subnegotiation
+      when TELNET_SB
+        me.state = :sb
+      # negotiation commands
+      when TELNET_WILL
+        me.state = :will
+      when TELNET_WONT
+        me.state = :wont
+      when TELNET_DO
+        me.state = :do
+      when TELNET_DONT
+        me.state = :dont
+      # IAC escaping 
+      when TELNET_IAC
+        me.buffer << TELNET_IAC.chr
+        send_event(:data, me.buffer) unless me.buffer.empty?
+        me.buffer = ""
+        me.state = :data
+      # some other command
+      else
+        send_event(:iac, byte)
+        me.state = :data
+      end
+
+    # negotiation received before
+    when :will, :wont, :do, :dont
+      do_negotiate(byte)
+      me.state = :data
+    # subnegotiation started, determine option to subnegotiate
+    when :sb
+      me.sb_telopt = byte
+      me.state     = :sb_data
+    # subnegotiation data, buffer bytes until the end request 
+    when :sb_data
+      # IAC command in subnegotiation -- either IAC SE or IAC IAC
+      if (byte == TELNET_IAC)
+        me.state = :sb_data_iac
+      elsif (me.sb_telopt == TELNET_TELOPT_COMPRESS && byte == TELNET_WILL)
+        # MCCPv1 defined an invalid subnegotiation sequence (IAC SB 85 WILL SE) 
+        # to start compression. Catch and discard this case, only support 
+        # MMCPv2.
+        me.state = data
+      else 
+        me.buffer << byte
+      end
+
+    # IAC received inside a subnegotiation
+    when :sb_data_iac
+      case byte
+        # end subnegotiation
+        when TELNET_SE
+          me.state = :data
+          # process subnegotiation
+          compress = do_subnegotiate(me.buffer)
+          # if compression was negotiated, the rest of the stream is compressed
+          # and processing it requires decompressing it. Return true to signal 
+          # this.
+          me.buffer = ""
+          return true if compress
+        # escaped IAC byte
+        when TELNET_IAC
+        # push IAC into buffer */
+          me.buffer << byte
+          me.state = :sb_data
+        # something else -- protocol error.  attempt to process
+        # content in subnegotiation buffer, then evaluate the
+        # given command as an IAC code.
+        else
+          log_error("Unexpected byte after IAC inside SB: %d", byte)
+          me.state = :iac
+          # subnegotiate with the buffer anyway, even though it's an error
+          compress = do_subnegotiate(me.buffer)
+          # if compression was negotiated, the rest of the stream is compressed
+          # and processing it requires decompressing it. Return true to signal 
+          # this.
+          me.buffer = ""
+          return true if compress
+        end
+    when :data  
+      # buffer any other bytes
+      me.buffer << byte
+    else 
+      # programing error, shouldn't happen
+      raise "Error in telet state machine!"
+    end
+    # return false to signal compression needn't start
+    return false
+  end
+  
+  def process_bytes(bytes)
+    # I have a feeling this way of handling strings isn't very efficient.. :p
+    arr = bytes.bytes.to_a
+    byte = arr.shift
+    while byte
+      compress = process_byte(byte)
+      if compress
+        # paper over this for a while... 
+        new_bytes = Zlib.inflate(arr.pack('c*')) rescue nil
+        if new_bytes
+          arr = new_bytes.bytes.to_a
+        end
+      end
+      byte = arr.shift    
+    end
+    send_event(:data, me.buffer) unless me.buffer.empty?
+    me.buffer = ""
+  end
+  
+  # Call this when the server receives data from the client
+  def telnet_receive(data)
+    # the COMPRESS2 protocol seems to be half-duplex in that only 
+    # the server's data stream is compressed (unless maybe if the client
+    # is asked to also compress with a DO command ?)
+    process_bytes(data)
+  end
+  
+  # Send a bytes array (raw) to the client
+  def telnet_send_bytes(*bytes)
+    s     = bytes.pack('C*')
+    send_raw(s)
+  end
+  
+  # send an iac command 
+  def telnet_send_iac(cmd)
+    telnet_send_bytes(TELNET_IAC, cmd)
+  end
+
+  # send negotiation
+  def telnet_send_negotiate(cmd, telopt)
+    # get current option states
+    q = rfc1143_get(telopt)
+    unless q
+      rfc1143_set(telopt)
+      q = rfc1143_get(telopt)
+    end
+    
+    act, arg = nil, nil
+    case cmd
+      when TELNET_WILL
+        act, arg = q.send_will
+      when TELNET_WONT
+        act, arg = q.send_wont
+      when TELNET_DO
+        act, arg = q.send_do
+      when TELNET_DONT
+        act, arg = q.send_dont    
+    end
+        
+    return false unless act    
+    telnet_send_bytes(TELNET_IAC, act, telopt)
+  end
+        
+
+  # send non-command data (escapes IAC bytes)
+  def telnet_send(buffer)
+    send_escaped(buffer)
+  end
+  
+  # send subnegotiation header
+  def telnet_begin_sb(telopt)
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, telopt)
+  end
+
+  # send subnegotiation ending
+  def telnet_end_sb()
+    telnet_send_bytes(TELNET_IAC, TELNET_SE)
+  end
+
+
+  # send complete subnegotiation
+  def telnet_subnegotiation(telopt, buffer = nil)
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, telopt)
+    telnet_send(buffer) if buffer;
+    telnet_send_bytes(TELNET_IAC, TELNET_SE)
+  end
+  
+  # start compress2 compression
+  def telnet_begin_compress2() 
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, TELNET_TELOPT_COMPRESS2, TELNET_IAC, TELNET_SE);
+    me.compress = true
+  end
+  
+  # send formatted data
+  def telnet_raw_printf(fmt, *args)
+    buf   = sprintf(fmt, *args)
+    telnet_send(buf)
+  end
+
+  CRLF  = "\r\n"
+  CRNUL = "\r\0"
+  
+  # send formatted data with \r and \n translation in addition to IAC IAC 
+  def telnet_printf(fmt, *args)
+    buf   = sprintf(fmt, *args)
+    buf.gsub!("\r", CRNUL)
+    buf.gsub!("\n", CRLF)
+    telnet_send(buf)
+  end
+
+  # begin NEW-ENVIRON subnegotation
+  def telnet_begin_newenviron(cmd)
+    telnet_begin_sb(TELNET_TELOPT_NEW_ENVIRON)
+    telnet_send_bytes(cmd)
+  end
+  
+  # send a NEW-ENVIRON value
+  def telnet_newenviron_value(type, value)
+    telnet_send_bytes(type)
+    telnet_send(string)
+  end
+  
+  # send TERMINAL-TYPE SEND command
+  def telnet_ttype_send() 
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, TELNET_TELOPT_TTYPE, TELNET_TTYPE_SEND, TELNET_IAC, TELNET_SE)
+  end  
+  
+  # send TERMINAL-TYPE IS command 
+  def telnet_ttype_is(ttype)
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, TELNET_TELOPT_TTYPE, TELNET_TTYPE_IS)
+    telnet_send(ttype)
+  end
+  
+  # send MSSP data
+  def telnet_send_mssp(mssp)
+    buf = ""
+    mssp.each do | key, val| 
+      buf << TELNET_MSSP_VAR.chr
+      buf << key
+      buf << TELNET_MSSP_VAL.chr
+      buf << val      
+    end
+    telnet_subnegotiation(TELNET_TELOPT_MSSP, buf)
+  end
+
+end
+
+
+

+ 21 - 0
src/woe/woe.go

@@ -0,0 +1,21 @@
+package main
+
+// import "fmt"
+import "github.com/beoran/woe/server"
+import "github.com/beoran/woe/monolog"
+
+
+
+func main() {
+    monolog.Setup("woe.log", true, false)
+    defer monolog.Close()
+    monolog.Info("Starting WOE!")
+    monolog.Info("Server runs at port %d!", 7000)
+    woe, err := server.NewServer(":7000")
+    if err != nil {
+        panic(err)
+    }
+    defer woe.Close()
+    woe.Serve()
+}
+