using System; using System.Collections.Generic; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using UnityEngine; namespace Mongoose { // See https://github.com/cesanta/mongoose for code and examples // mongoose.h :: struct mg_connection { struct mg_header } [StructLayout(LayoutKind.Sequential)] public struct MongooseHeader { [MarshalAs(UnmanagedType.LPStr)] public string name; [MarshalAs(UnmanagedType.LPStr)] public string value; }; // mongoose.h :: struct mg_connection [StructLayout(LayoutKind.Sequential)] public struct MongooseConnection { [MarshalAs(UnmanagedType.LPStr)] public string request_method; [MarshalAs(UnmanagedType.LPStr)] public string uri; [MarshalAs(UnmanagedType.LPStr)] public string http_version; [MarshalAs(UnmanagedType.LPStr)] public string query_string; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)] public char[] remote_ip; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)] public char[] local_ip; [MarshalAs(UnmanagedType.U2)] public UInt16 remote_port; [MarshalAs(UnmanagedType.U2)] public UInt16 local_port; [MarshalAs(UnmanagedType.I4)] public int num_headers; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)] public MongooseHeader[] http_headers; public IntPtr content; public System.UInt64 content_len; [MarshalAs(UnmanagedType.I4)] public int is_websocket; [MarshalAs(UnmanagedType.I4)] public int status_code; [MarshalAs(UnmanagedType.I4)] public int wsbits; public IntPtr server_param; public IntPtr connection_param; public IntPtr callback_param; }; public class WebServer { public string BaseURI() { foreach (NetworkInterface item in NetworkInterface.GetAllNetworkInterfaces()) if (item.NetworkInterfaceType == NetworkInterfaceType.Ethernet) foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses) if (ip.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) return "http://" + ip.Address.ToString() + ":" + getOption("listening_port"); return "http://127.0.0.1:" + getOption("listening_port"); } public enum MongooseResult { MG_FALSE, MG_TRUE, MG_MORE }; private enum MongooseEvent { MG_POLL = 100, // If callback returns MG_TRUE connection closes // after all of data is sent MG_CONNECT, // If callback returns MG_FALSE, connect fails MG_AUTH, // If callback returns MG_FALSE, authentication fails MG_REQUEST, // If callback returns MG_FALSE, Mongoose continues with req MG_REPLY, // If callback returns MG_FALSE, Mongoose closes connection MG_RECV, // Mongoose has received POST data chunk. // Callback should return a number of bytes to discard from // the receive buffer, or -1 to close the connection. MG_CLOSE, // Connection is closed, callback return value is ignored MG_WS_HANDSHAKE, // New websocket connection, handshake request MG_WS_CONNECT, // New websocket connection established MG_HTTP_ERROR // If callback returns MG_FALSE, Mongoose continues with err }; // Possible websocket opcodes. These are available as the right four bits of // the websocket status in the connection after a MG_REQUEST coming from a // websocket. The first four bits are: // bit 0. 1 if this is the last frame in the message // bits 1-3. reserved private enum MongooseWebsockOpcode { WEBSOCKET_OPCODE_CONTINUATION = 0x0, WEBSOCKET_OPCODE_TEXT = 0x1, WEBSOCKET_OPCODE_BINARY = 0x2, WEBSOCKET_OPCODE_CONNECTION_CLOSE = 0x8, WEBSOCKET_OPCODE_PING = 0x9, WEBSOCKET_OPCODE_PONG = 0xa }; #if WEBTRACE static public System.IO.StreamWriter logFile = new System.IO.StreamWriter("debugCS.log"); #endif [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall)] private static extern IntPtr mg_create_server(IntPtr user_data, MongooseEventHandler eh); [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall)] private static extern int mg_poll_server(IntPtr server, int milli); [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] private static extern IntPtr mg_set_option(IntPtr server, string name, string value); [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] private static extern IntPtr mg_get_option(IntPtr server, string name); [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] private static extern ulong mg_send_header(IntPtr conn, string name, string value); // Note: This could be binary data in the C++ code. [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] private static extern ulong mg_send_data(IntPtr conn, string data, int length); [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] public static extern ulong mg_websocket_write(IntPtr conn, int opCode, string data, ulong dataLength); [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall)] private static extern void mg_destroy_server(IntPtr server); [DllImport("Mongoose", CallingConvention = CallingConvention.StdCall)] public static extern IntPtr mg_next(IntPtr server, IntPtr connection); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int MongooseEventHandler(IntPtr c, int ev); private IntPtr _server; private MongooseEventHandler _delegateInstance; // Connection maps for keeping track of clients map protected Dictionary _connections = new Dictionary(); protected Dictionary _wsConnections = new Dictionary(); // Default constructor - serves documents from the current directory and listens on port 8080 public bool startWebServer() { return startWebServer(".", 8080); } public bool startWebServer(string documentRoot, int port) { _delegateInstance = new MongooseEventHandler(EventHandler); _server = mg_create_server(IntPtr.Zero, _delegateInstance); if (_server == (IntPtr)0) { Debug.LogError("Coudln't start web server"); return false; } return setOption("document_root", ".") && setOption("listening_port", "8080") && setOption("enable_directory_listing", "no"); #if WEBTRACE logFile.WriteLine("Server started"); logFile.Flush(); #endif } public string getOption(string option) { return Marshal.PtrToStringAnsi(mg_get_option(_server, option)); } public bool setOption(string option, string value) { IntPtr retVal = mg_set_option(_server, option, value); if ( retVal != (IntPtr)0 ) { Debug.LogWarning("Web server couldn't set option: "+option+" to value:"+value+". Error: "+Marshal.PtrToStringAnsi(retVal)); #if WEBTRACE string errorMsg = Marshal.PtrToStringAnsi(retVal); logFile.WriteLine("Error setting option: " + option + ". " + errorMsg); logFile.Flush(); #endif return false; } return true; } // Send a header to a normal connection. Use this to send headers after a MG_REQUEST call // if you are not going to serve up a file public ulong sendHeader(int port, string name, string value) { if ( _connections.ContainsKey(port) ) return mg_send_header(_connections[port], name, value); else return 0; } // Send data to a normal connection. Use this to send data after a MG_REQUEST call // if you are not going to serve up a file // Returns number of bytes sent public ulong sendData(int port, string data) { if ( _connections.ContainsKey(port) ) return mg_send_data(_connections[port], data, data.Length); else return 0; } // Send data to a websocket connection. This can be called at any time after the connection is // made public ulong sendWebsocketData(int port, string message) { if (_wsConnections.ContainsKey(port)) return mg_websocket_write(_wsConnections[port], 1, message, (ulong)message.Length); else return 0; } public void websocketBroadcast(string message) { foreach ( var connection in _wsConnections ) { #if WEBTRACE logFile.WriteLine("Writing to websocket on port=" + connection.Key + " msg=" + message); #endif mg_websocket_write(connection.Value, (int)MongooseWebsockOpcode.WEBSOCKET_OPCODE_TEXT, message, (ulong)message.Length); } } private List _connectionsToClose = new List(); public void closeConnection(int port) { if ( _wsConnections.ContainsKey(port) ) { mg_websocket_write(_wsConnections[port], (int) MongooseWebsockOpcode.WEBSOCKET_OPCODE_CONNECTION_CLOSE, "", 0); } else { _connectionsToClose.Add(port); } } private Dictionary _currentConnections = new Dictionary(); public void poll(int msecToWait) { try { _currentConnections.Clear(); #if WEBTRACE logFile.WriteLine("Calling mg_poll_server on: "+_server.ToString()); logFile.WriteLine("Document root=" + getOption("document_root")); logFile.Flush(); #endif mg_poll_server(_server, msecToWait); // Process the difference between the old connections and the new connections // Remove lost connections foreach (var connection in _wsConnections) { if (!_currentConnections.ContainsKey(connection.Key)) { _wsConnections.Remove(connection.Key); _connections.Remove(connection.Key); handleClosedConnection(connection.Key, true); } } foreach (var connection in _connections) { if (!_currentConnections.ContainsKey(connection.Key)) { _connections.Remove(connection.Key); handleClosedConnection(connection.Key, false); } } } catch (Exception e) { Debug.LogException(e); #if WEBTRACE logFile.WriteLine("Exception caught: " + e.Message + "\nStack WEBTRACE: " + e.StackTrace); logFile.Flush(); #endif } } public void close() { mg_destroy_server(_server); _server = (IntPtr)0; } // Every request is preceded by an authoriazation check. Return FALSE to deny - web browser will // ask for credentials. virtual public MongooseResult handleAuthEvent(MongooseConnection conn) { return MongooseResult.MG_TRUE; } // When a request is authorized, this functionn is called to fufill the request. // Return MG_TRUE to signify that the request was satisfied by this function // Return MG_FALSE to let the web server handle the request (return the file requested or error page) virtual public MongooseResult handleRequestEvent(IntPtr connPtr, MongooseConnection conn) { if (conn.is_websocket == 1) { string msg = Marshal.PtrToStringAnsi(conn.content, (int)conn.content_len); return handleWSRequest(conn.remote_port, msg); } else return MongooseResult.MG_FALSE; } virtual public MongooseResult handleWSRequest(int remotePort, string message) { return MongooseResult.MG_TRUE; } virtual public void handleClosedConnection(int remotePort, bool isWebsocket) { #if WEBTRACE logFile.WriteLine(" handleClosedConnection: port=" + remotePort + " isWS=" + isWebsocket); logFile.Flush(); #endif } virtual public void handleNewConnection(int remotePort, bool isWebsocket) { #if WEBTRACE logFile.WriteLine(" handleNewConnection: port=" + remotePort + " isWS=" + isWebsocket); logFile.Flush(); #endif } // Websocket connection requested. // Return MG_TRUE to deny the request and MG_FALSE to accept // The websocket connection will still be live for a while - we get the connect, auth, recv and a couple polls before it closes virtual public MongooseResult handleWSHandshakeEvent(IntPtr connPtr, MongooseConnection conn) { return MongooseResult.MG_FALSE; } // Websocket connection made. This function can send data to the connection. virtual public MongooseResult handleWSConnectEvent(IntPtr connPtr, MongooseConnection conn) { return MongooseResult.MG_TRUE; } // Webserver can't/wont fufill a request and is going to send an error // This function can send a different response or return MG_FALSE to let the server send the error virtual public MongooseResult handleHTTPError(IntPtr connPtr, MongooseConnection conn) { #if WEBTRACE logFile.WriteLine(" Server sending HTTP ERROR " + conn.status_code); logFile.WriteLine(" conn.num_headers=" + conn.num_headers); logFile.WriteLine(" conn.content_len=" + conn.content_len); if ( conn.content_len>0 ) logFile.WriteLine(" conn.content=" + Marshal.PtrToStringAnsi(conn.content, (int)conn.content_len)); #endif return MongooseResult.MG_FALSE; } private List _deniedConnections = new List(); private int EventHandler(IntPtr conn_ptr, int ev) { #if WEBTRACE logFile.WriteLine("document_root=" + getOption("document_root")); logFile.Flush(); #endif MongooseEvent evt = (MongooseEvent)ev; #if WEBTRACE logFile.Write("EventHandler evt=" + evt.ToString() + " "); logFile.Flush(); #endif if (conn_ptr == (IntPtr)0) return 0; MongooseConnection conn = (MongooseConnection) System.Runtime.InteropServices.Marshal.PtrToStructure( conn_ptr, typeof(MongooseConnection)); #if WEBTRACE logFile.WriteLine("port = " + conn.remote_port + "(" + conn.is_websocket + ") request=" + conn.request_method + " " + conn.uri); logFile.Flush(); #endif switch (evt) { case MongooseEvent.MG_AUTH: return (int)handleAuthEvent(conn); case MongooseEvent.MG_POLL: // Sent for every connection for every poll. This is used to signal data transfer complete if ( _connectionsToClose.Contains(conn.remote_port) ) { _connectionsToClose.Remove(conn.remote_port); return (int)MongooseResult.MG_TRUE; } _currentConnections[conn.remote_port] = conn_ptr; if ( !_connections.ContainsKey(conn.remote_port)) { _connections.Add(conn.remote_port, conn_ptr); handleNewConnection(conn.remote_port, conn.is_websocket == 1); } return (int)MongooseResult.MG_FALSE; case MongooseEvent.MG_REQUEST: // A request has been made by the client (already authorized) return (int)handleRequestEvent(conn_ptr, conn); case MongooseEvent.MG_RECV: // Data has been received by the server. This is websocket/post/upload data. Used to handle and discard received data before it is sent to the REQUEST handler. return 0; // Return number of bytes to discard case MongooseEvent.MG_CLOSE: // Connection closed if (_currentConnections.ContainsKey(conn.remote_port)) _currentConnections.Remove(conn.remote_port); if (_connections.ContainsKey(conn.remote_port)) _connections.Remove(conn.remote_port); if (_wsConnections.ContainsKey(conn.remote_port)) _wsConnections.Remove(conn.remote_port); if (_deniedConnections.Contains(conn.remote_port)) _deniedConnections.Remove(conn.remote_port); handleClosedConnection(conn.remote_port, conn.is_websocket == 1); return 0; // Return value ignored case MongooseEvent.MG_WS_HANDSHAKE: // Accept or deny a websocket connection MongooseResult response = handleWSHandshakeEvent(conn_ptr, conn); if (response == MongooseResult.MG_TRUE) _deniedConnections.Add(conn.remote_port); return (int)response; case MongooseEvent.MG_WS_CONNECT: // Websocket connection established if (!_deniedConnections.Contains(conn.remote_port)) { _wsConnections.Add(conn.remote_port, conn_ptr); handleNewConnection(conn.remote_port, true); return (int)handleWSConnectEvent(conn_ptr, conn); } else return 0; case MongooseEvent.MG_HTTP_ERROR: // Webserver can't/wont fufill a request and is going to send an error return (int)handleHTTPError(conn_ptr, conn); case MongooseEvent.MG_REPLY: // A response was received by a remote host. Not used return (int)MongooseResult.MG_FALSE; case MongooseEvent.MG_CONNECT: // Connection to remote host accepted. Not used return (int)MongooseResult.MG_FALSE; } return 0; } } }