lib: intro the web server nitcorn
authorAlexis Laferrière <alexis.laf@xymus.net>
Tue, 22 Jul 2014 21:41:40 +0000 (17:41 -0400)
committerAlexis Laferrière <alexis.laf@xymus.net>
Wed, 30 Jul 2014 13:15:51 +0000 (09:15 -0400)
Signed-off-by: Alexis Laferrière <alexis.laf@xymus.net>

lib/nitcorn/README.md [new file with mode: 0644]
lib/nitcorn/file_server.nit [new file with mode: 0644]
lib/nitcorn/http_request.nit [new file with mode: 0644]
lib/nitcorn/http_response.nit [new file with mode: 0644]
lib/nitcorn/media_types.nit [new file with mode: 0644]
lib/nitcorn/nitcorn.nit [new file with mode: 0644]
lib/nitcorn/reactor.nit [new file with mode: 0644]
lib/nitcorn/server_config.nit [new file with mode: 0644]
lib/nitcorn/sessions.nit [new file with mode: 0644]

diff --git a/lib/nitcorn/README.md b/lib/nitcorn/README.md
new file mode 100644 (file)
index 0000000..f9e952d
--- /dev/null
@@ -0,0 +1,30 @@
+The nitcorn Web server framework creates server-side Web apps in Nit
+
+# Examples
+
+Want to see `nitcorn` in action? Examples are available at ../../examples/nitcorn/src/.
+
+# Features and TODO list
+
+ - [x] Virtual hosts and routes
+ - [x] Configuration change on the fly
+ - [x] Sessions
+ - [x] Reading cookies
+ - [ ] Full cookie support
+ - [ ] Close interfaces on the fly
+ - [ ] Better logging
+ - [ ] Info/status page
+ - [ ] `ProxyAction` to redirect a request to an external server
+ - [ ] `ModuleAction` which forwards the request to an independant Nit program
+
+## Bugs / Limitations
+
+* The size of requests is limited, so no big uploads
+
+# Credits
+
+This nitcorn library is a fork from an independant project originally created in 2013 by
+Jean-Philippe Caissy, Guillaume Auger, Frederic Sevillano, Justin Michaud-Ouellette,
+Stephan Michaud and Maxime Bélanger.
+
+It has been adapted to a library, and is currently maintained, by Alexis Laferrière.
diff --git a/lib/nitcorn/file_server.nit b/lib/nitcorn/file_server.nit
new file mode 100644 (file)
index 0000000..348383e
--- /dev/null
@@ -0,0 +1,128 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Provides the `FileServer` action, which is a standard and minimal file server
+module file_server
+
+import reactor
+import sessions
+import media_types
+
+redef class String
+       # Returns a `String` copy of `self` without any of the prefixed '/'s
+       #
+       # Examples:
+       #
+       #     assert "/home/".strip_start_slashes == "home/"
+       #     assert "////home/".strip_start_slashes == "home/"
+       #     assert "../home/".strip_start_slashes == "../home/"
+       fun strip_start_slashes: String
+       do
+               for i in chars.length.times do if chars[i] != '/' then return substring_from(i)
+               return ""
+       end
+end
+
+# A simple file server
+class FileServer
+       super Action
+
+       # Root of `self` file system
+       var root: String
+
+       redef fun answer(request, turi)
+       do
+               var response
+
+               var local_file = root.join_path(turi.strip_start_slashes)
+               local_file = local_file.simplify_path
+
+               # HACK
+               if turi == "/" then local_file = root
+
+               # Is it reachable?
+               if local_file.has_prefix(root) then
+                       # Does it exists?
+                       if local_file.file_exists then
+                               response = new HttpResponse(200)
+
+                               if local_file.file_stat.is_dir then
+                                       # Show index.html instead of the directory listing
+                                       var index_file = local_file.join_path("index.html")
+                                       if index_file.file_exists then
+                                               local_file = index_file
+                                       else
+                                               index_file = local_file.join_path("index.htm")
+                                               if index_file.file_exists then local_file = index_file
+                                       end
+                               end
+
+                               if local_file.file_stat.is_dir then
+                                       # Show the directory listing
+                                       var title = turi
+                                       var files = local_file.files
+
+                                       var links = new Array[String]
+                                       if local_file.length > 1 then
+                                               # The extra / is a hack
+                                               var path = "/" + (turi + "/..").simplify_path
+                                               links.add "<a href=\"{path}\">..</a>"
+                                       end
+                                       for file in files do
+                                               var path = (turi + "/" + file).simplify_path
+                                               links.add "<a href=\"{path}\">{file}</a>"
+                                       end
+
+                                       response.body = """
+<!DOCTYPE html>
+<head>
+       <meta charset="utf-8">
+       <meta http-equiv="X-UA-Compatible" content="IE=edge">
+       <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+       <title>{{{title}}}</title>
+</head>
+<body>
+       <div class="container">
+               <h1>{{{title}}}</h1>
+               <ul>
+                       <li>{{{links.join("</li>\n\t\t\t<li>")}}}</li>
+               </ul>
+       </div>
+</body>
+</html>"""
+
+                                       response.header["Content-Type"] = media_types["html"].as(not null)
+                               else
+                                       # It's a single file
+                                       var file = new IFStream.open(local_file)
+                                       response.body = file.read_all
+
+                                       var ext = local_file.file_extension
+                                       if ext != null then
+                                               var media_type = media_types[ext]
+                                               if media_type != null then response.header["Content-Type"] = media_type
+                                       end
+
+                                       file.close
+                               end
+
+                       else response = new HttpResponse(404)
+               else response = new HttpResponse(403)
+
+               return response
+       end
+end
diff --git a/lib/nitcorn/http_request.nit b/lib/nitcorn/http_request.nit
new file mode 100644 (file)
index 0000000..6431ace
--- /dev/null
@@ -0,0 +1,204 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2013 Frederic Sevillano
+# Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Provides the `HttpRequest` class and services to create it
+module http_request
+
+import standard
+
+# A request received over HTTP, is build by `HttpRequestParser`
+class HttpRequest
+       private init do end
+
+       # HTTP protocol version
+       var http_version: String
+
+       # Method of this request (GET or POST)
+       var method: String
+
+       # The host targetter by this request (usually the server)
+       var host: String
+
+       # The full URL requested by the client (including the `query_string`)
+       var url: String
+
+       # The resource requested by the client (only the page, not the `query_string`)
+       var uri: String
+
+       # The string following `?` in the requested URL
+       var query_string = ""
+
+       # The header of this request
+       var header = new HashMap[String, String]
+
+       # The content of the cookie of this request
+       var cookie = new HashMap[String, String]
+
+       # The arguments passed with the GET method,
+       var get_args = new HashMap[String, String]
+
+       # The arguments passed with the POST method
+       var post_args = new HashMap[String, String]
+end
+
+# Utility class to parse a request string and build a `HttpRequest`
+#
+# The main method is `parse_http_request`.
+class HttpRequestParser
+       # The current `HttpRequest` under construction
+       private var http_request: HttpRequest
+
+       # Untreated body
+       private var body = ""
+
+       # Lines of the header
+       private var header_fields = new Array[String]
+
+       # Words of the first line
+       private var first_line = new Array[String]
+
+       init do end
+
+       fun parse_http_request(full_request: String): nullable HttpRequest
+       do
+               clear_data
+
+               var http_request = new HttpRequest
+               self.http_request = http_request
+
+               segment_http_request(full_request)
+
+               # Parse first line, looks like "GET dir/index.html?user=xymus HTTP/1.0"
+               http_request.method = first_line[0]
+               http_request.url = first_line[1]
+               http_request.http_version = first_line[2]
+
+               # GET args
+               if http_request.url.has('?') then
+                       http_request.uri = first_line[1].substring(0, first_line[1].index_of('?'))
+                       http_request.query_string = first_line[1].substring_from(first_line[1].index_of('?')+1)
+                       http_request.get_args = parse_url
+               else
+                       http_request.uri = first_line[1]
+               end
+
+               # POST args
+               if http_request.method == "POST" then
+                       var lines = body.split_with('&')
+                       for line in lines do
+                               var parts = line.split_once_on('=')
+                               if parts.length > 1 then
+                                       var decoded = parts[1].replace('+', " ").from_percent_encoding
+                                       if decoded == null then
+                                               print "decode error"
+                                               continue
+                                       end
+                                       http_request.post_args[parts[0]] = decoded
+                               else
+                                       print "POST Error: {line} format error on {line}"
+                               end
+                       end
+               end
+
+               # Headers
+               for i in header_fields do
+                       var temp_field = i.split_with(": ")
+
+                       if temp_field.length == 2 then
+                               http_request.header[temp_field[0]] = temp_field[1]
+                       end
+               end
+
+               # Cookies
+               if http_request.header.keys.has("Cookie") then
+                       var cookie = http_request.header["Cookie"]
+                       for couple in cookie.split_with(';') do
+                               var words = couple.trim.split_with('=')
+                               if words.length != 2 then continue
+                               http_request.cookie[words[0]] = words[1]
+                       end
+               end
+
+               return http_request
+       end
+
+       private fun clear_data
+       do
+               first_line.clear
+               header_fields.clear
+       end
+
+       private fun segment_http_request(http_request: String): Bool
+       do
+               var header_end = http_request.search("\r\n\r\n")
+
+               if header_end == null then
+                       header_fields = http_request.split_with("\r\n")
+               else
+                       header_fields = http_request.substring(0, header_end.from).split_with("\r\n")
+                       body = http_request.substring(header_end.after, http_request.length-1)
+               end
+
+               # If a line of the http_request is long it may change line, it has " " at the
+               # end to indicate this. This section turns them into 1 line.
+               if header_fields.length > 1 and header_fields[0].has_suffix(" ") then
+                       var temp_req = header_fields[0].substring(0, header_fields[0].length-1) + header_fields[1]
+
+                       first_line  = temp_req.split_with(' ')
+                       header_fields.shift
+                       header_fields.shift
+
+                       if first_line.length != 3 then return false
+               else
+                       first_line = header_fields[0].split_with(' ')
+                       header_fields.shift
+
+                       if first_line.length != 3 then return false
+               end
+
+               # Cut off the header in lines
+               var pos = 0
+               while pos < header_fields.length do
+                       if pos < header_fields.length-1 and header_fields[pos].has_suffix(" ") then
+                               header_fields[pos] = header_fields[pos].substring(0, header_fields[pos].length-1) + header_fields[pos+1]
+                               header_fields.remove_at(pos+1)
+                               pos = pos-1
+                       end
+                       pos = pos+1
+               end
+
+               return true
+       end
+
+       # Extract args from the URL
+       private fun parse_url: HashMap[String, String]
+       do
+               var query_strings = new HashMap[String, String]
+
+               if http_request.url.has('?') then
+                       var get_args = http_request.query_string.split_with("&")
+                       for param in get_args do
+                               var key_value = param.split_with("=")
+                               if key_value.length < 2 then continue
+                               query_strings[key_value[0]] = key_value[1]
+                       end
+               end
+
+               return query_strings
+       end
+end
diff --git a/lib/nitcorn/http_response.nit b/lib/nitcorn/http_response.nit
new file mode 100644 (file)
index 0000000..666974b
--- /dev/null
@@ -0,0 +1,132 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2013 Frederic Sevillano
+# Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Provides the `HttpResponse` class and `http_status_codes`
+module http_response
+
+# A response to send over HTTP
+class HttpResponse
+
+       # HTTP protocol version
+       var http_version = "HTTP/1.0" is writable
+
+       # Status code of this response (200, 404, etc.)
+       var status_code: Int is writable
+
+       # Return the message associated to `status_code`
+       fun status_message: nullable String do return http_status_codes[status_code]
+
+       # Headers of this response as a `Map`
+       var header = new HashMap[String, String]
+
+       # Body of this response
+       var body = "" is writable
+
+       # Finalize this response before sending it over HTTP
+       fun finalize
+       do
+               # Set the content length if not already set
+               if not header.keys.has("Content-Length") then
+                       header["Content-Length"] = body.length.to_s
+               end
+
+               # Set server ID
+               if not header.keys.has("Server") then header["Server"] = "unitcorn"
+       end
+
+       # Get this reponse as a string according to HTTP protocol
+       redef fun to_s: String
+       do
+               finalize
+
+               var buf = new FlatBuffer
+               buf.append("{http_version} {status_code} {status_message or else ""}\r\n")
+               for key, value in header do
+                       buf.append("{key}: {value}\r\n")
+               end
+               buf.append("\r\n{body}")
+               return buf.to_s
+       end
+end
+
+# Helper class to associate HTTP status code to their message
+#
+# You probably want the default instance available as the top-level method
+# `http_status_codes`.
+class HttpStatusCodes
+
+       # All know code and their message
+       var codes = new HashMap[Int, String]
+
+       protected init do insert_status_codes
+
+       # Get the message associated to the status `code`, return `null` in unknown
+       fun [](code: Int): nullable String
+       do
+               if codes.keys.has(code) then
+                       return codes[code]
+               else return null
+       end
+
+       private fun insert_status_codes
+       do
+               codes[100] = "Continue"
+               codes[101] = "Switching Protocols"
+               codes[200] = "OK"
+               codes[201] = "Created"
+               codes[202] = "Accepted"
+               codes[203] = "Non-Authoritative Information"
+               codes[204] = "No Content"
+               codes[205] = "Reset Content"
+               codes[206] = "Partial Content"
+               codes[300] = "Multiple Choices"
+               codes[301] = "Moved Permanently"
+               codes[302] = "Found"
+               codes[303] = "See Other"
+               codes[304] = "Not Modified"
+               codes[305] = "Use Proxy"
+               codes[307] = "Temporary Redirect"
+               codes[400] = "Bad Request"
+               codes[401] = "Unauthorized"
+               codes[402] = "Payment Requred"
+               codes[403] = "Forbidden"
+               codes[404] = "Not Found"
+               codes[405] = "Method Not Allowed"
+               codes[406] = "Not Acceptable"
+               codes[407] = "Proxy Authentication Required"
+               codes[408] = "Request Timeout"
+               codes[409] = "Conflict"
+               codes[410] = "Gone"
+               codes[411] = "Length Required"
+               codes[412] = "Precondition Failed"
+               codes[413] = "Request Entity Too Large"
+               codes[414] = "Request-URI Too Long"
+               codes[415] = "Unsupported Media Type"
+               codes[416] = "Requested Range Not Satisfiable"
+               codes[417] = "Expectation Failed"
+               codes[500] = "Internal Server Error"
+               codes[501] = "Not Implemented"
+               codes[502] = "Bad Gateway"
+               codes[503] = "Service Unavailable"
+               codes[504] = "Gateway Timeout"
+               codes[505] = "HTTP Version Not Supported"
+       end
+end
+
+# Get the default instance of `HttpStatusCodes`
+fun http_status_codes: HttpStatusCodes do return once new HttpStatusCodes
diff --git a/lib/nitcorn/media_types.nit b/lib/nitcorn/media_types.nit
new file mode 100644 (file)
index 0000000..bf5d74a
--- /dev/null
@@ -0,0 +1,101 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2013 Justin Michaud-Ouellette
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Services to identify Internet media types (or MIME types, Content-types)
+module media_types
+
+# Map of known MIME types
+class MediaTypes
+       protected var types = new HashMap[String, String]
+
+       # Get the type/subtype associated to a file extension `ext`
+       fun [](ext: String): nullable String
+       do
+               if types.keys.has(ext) then return types[ext]
+               return null
+       end
+
+       init
+       do
+               types["html"]       = "text/html"
+               types["htm"]        = "text/html"
+               types["shtml"]      = "text/html"
+               types["css"]        = "text/css"
+               types["xml"]        = "text/xml"
+               types["rss"]        = "text/xml"
+               types["gif"]        = "image/gif"
+               types["jpg"]        = "image/jpeg"
+               types["jpeg"]       = "image/jpeg"
+               types["js"]         = "application/x-javascript"
+               types["txt"]        = "text/plain"
+               types["htc"]        = "text/x-component"
+               types["mml"]        = "text/mathml"
+               types["png"]        = "image/png"
+               types["ico"]        = "image/x-icon"
+               types["jng"]        = "image/x-jng"
+               types["wbmp"]       = "image/vnd.wap.wbmp"
+               types["jar"]        = "application/java-archive"
+               types["war"]        = "application/java-archive"
+               types["ear"]        = "application/java-archive"
+               types["hqx"]        = "application/mac-binhex40"
+               types["pdf"]        = "application/pdf"
+               types["cco"]        = "application/x-cocoa"
+               types["jardiff"]    = "application/x-java-archive-diff"
+               types["jnlp"]       = "application/x-java-jnlp-file"
+               types["run"]        = "application/x-makeself"
+               types["pl"]         = "application/x-perl"
+               types["pm"]         = "application/x-perl"
+               types["pdb"]        = "application/x-pilot"
+               types["prc"]        = "application/x-pilot"
+               types["rar"]        = "application/x-rar-compressed"
+               types["rpm"]        = "application/x-redhat-package-manager"
+               types["sea"]        = "application/x-sea"
+               types["swf"]        = "application/x-shockwave-flash"
+               types["sit"]        = "application/x-stuffit"
+               types["tcl"]        = "application/x-tcl"
+               types["tk"]         = "application/x-tcl"
+               types["der"]        = "application/x-x509-ca-cert"
+               types["pem"]        = "application/x-x509-ca-cert"
+               types["crt"]        = "application/x-x509-ca-cert"
+               types["xpi"]        = "application/x-xpinstall"
+               types["zip"]        = "application/zip"
+               types["deb"]        = "application/octet-stream"
+               types["bin"]        = "application/octet-stream"
+               types["exe"]        = "application/octet-stream"
+               types["dll"]        = "application/octet-stream"
+               types["dmg"]        = "application/octet-stream"
+               types["eot"]        = "application/octet-stream"
+               types["iso"]        = "application/octet-stream"
+               types["img"]        = "application/octet-stream"
+               types["msi"]        = "application/octet-stream"
+               types["msp"]        = "application/octet-stream"
+               types["msm"]        = "application/octet-stream"
+               types["mp3"]        = "audio/mpeg"
+               types["ra"]         = "audio/x-realaudio"
+               types["mpeg"]       = "video/mpeg"
+               types["mpg"]        = "video/mpeg"
+               types["mov"]        = "video/quicktime"
+               types["flv"]        = "video/x-flv"
+               types["avi"]        = "video/x-msvideo"
+               types["wmv"]        = "video/x-ms-wmv"
+               types["asx"]        = "video/x-ms-asf"
+               types["asf"]        = "video/x-ms-asf"
+               types["mng"]        = "video/x-mng"
+       end
+end
+
+fun media_types: MediaTypes do return once new MediaTypes
diff --git a/lib/nitcorn/nitcorn.nit b/lib/nitcorn/nitcorn.nit
new file mode 100644 (file)
index 0000000..2a5b43a
--- /dev/null
@@ -0,0 +1,63 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# The nitcorn Web server framework creates server-side Web apps in Nit
+#
+# The main classes are:
+# * `Action` to answer to requests.
+# * `Route` to represent a path to an action.
+# * `VirtualHost` to listen on a specific interface and behave accordingly
+# * `HttpFactory` which is the base dispatcher class.
+#
+# Basic usage example:
+# ~~~~
+# class MyAction
+#      super Action
+#
+#      redef fun answer(http_request, turi)
+#      do
+#              var response = new HttpResponse(200)
+#              response.body = """
+#              <!DOCTYPE html>
+#              <head>
+#                      <meta charset="utf-8">
+#                      <title>Hello World</title>
+#              </head>
+#              <body>
+#                      <p>Hello World</p>
+#              </body>
+#              </html>"""
+#              return response
+#      end
+# end
+#
+# var vh = new VirtualHost("localhost:80")
+#
+# # Serve index.html with our custom handler
+# vh.routes.add new Route("/index.html", new MyAction)
+#
+# # Serve everything else with a standard `FileServer`
+# vh.routes.add new Route(null, new FileServer("/var/www/"))
+#
+# var factory = new HttpFactory.and_libevent
+# factory.config.virtual_hosts.add vh
+# factory.run
+# ~~~~
+module nitcorn
+
+import reactor
+import file_server
+import sessions
diff --git a/lib/nitcorn/reactor.nit b/lib/nitcorn/reactor.nit
new file mode 100644 (file)
index 0000000..a8f9236
--- /dev/null
@@ -0,0 +1,182 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Core of the `nitcorn` project, provides `HttpFactory` and `Action`
+module reactor
+
+import more_collections
+import libevent
+
+import server_config
+import http_request
+import http_response
+
+# A server handling a single connection
+class HttpServer
+       super Connection
+
+       # The associated `HttpFactory`
+       var factory: HttpFactory
+
+       init(buf_ev: NativeBufferEvent, factory: HttpFactory) do self.factory = factory
+
+       private var parser = new HttpRequestParser is lazy
+
+       redef fun read_callback(str)
+       do
+               # TODO support bigger inputs (such as big forms and file upload)
+
+               var request_object = parser.parse_http_request(str.to_s)
+
+               if request_object != null then delegate_answer request_object
+       end
+
+       # Answer to a request
+       fun delegate_answer(request: HttpRequest)
+       do
+               # Find target virtual host
+               var virtual_host = null
+               if request.header.keys.has("Host") then
+                       var host = request.header["Host"]
+                       if host.index_of(':') == -1 then host += ":80"
+                       for vh in factory.config.virtual_hosts do
+                               for i in vh.interfaces do if i.to_s == host then
+                                       virtual_host = vh
+                                       break label
+                               end
+                       end label
+               end
+
+               # Get a response from the virtual host
+               var response
+               if virtual_host != null then
+                       var route = virtual_host.routes[request.uri]
+                       if route != null then
+                               var handler = route.handler
+                               var root = route.path
+                               var turi
+                               if root != null then
+                                       turi = ("/" + request.uri.substring_from(root.length)).simplify_path
+                               else turi = request.uri
+                               response = handler.answer(request, turi)
+                       else response = new HttpResponse(405)
+               else response = new HttpResponse(405)
+
+               # Send back a response
+               write response.to_s
+               close
+       end
+end
+
+redef abstract class Action
+       # Handle a request with the relative URI `truncated_uri`
+       #
+       # `request` is fully formed request object and has a reference to the session
+       # if one preexists.
+       #
+       # `truncated_uri` is the ending of the fulle request URI, truncated from the route
+       # leading to this `Action`.
+       fun answer(request: HttpRequest, truncated_uri: String): HttpResponse is abstract
+end
+
+# Factory to create `HttpServer` instances, and hold the libevent base handler
+class HttpFactory
+       super ConnectionFactory
+
+       # Configuration of this server
+       #
+       # It should be populated after this object has instanciated
+       var config = new ServerConfig.with_factory(self)
+
+       # Instanciate a server and libvent
+       #
+       # You can use this to create the first `HttpFactory`, which is the most common.
+       init and_libevent do init(new NativeEventBase)
+
+       redef fun spawn_connection(buf_ev) do return new HttpServer(buf_ev, self)
+
+       # Launch the main loop of this server
+       fun run
+       do
+               event_base.dispatch
+               event_base.destroy
+       end
+end
+
+redef class ServerConfig
+       # Handle to retreive the `HttpFactory` on config change
+       private var factory: HttpFactory
+
+       private init with_factory(factory: HttpFactory) do self.factory = factory
+end
+
+redef class Sys
+       # Active listeners
+       private var listeners = new HashMap2[String, Int, ConnectionListener]
+
+       # Hosts needong each listener
+       private var listeners_count = new HashMap2[String, Int, Int]
+
+       # Activate a listener on `interfac` if there's not already one
+       private fun listen_on(interfac: Interface, factory: HttpFactory)
+       do
+               if interfac.registered then return
+
+               var name = interfac.name
+               var port = interfac.port
+
+               var listener = listeners[name, port]
+               if listener == null then
+                       listener = factory.bind_to(name, port)
+                       if listener != null then
+                               sys.listeners[name, port] = listener
+                               listeners_count[name, port] = 1
+                       end
+               else
+                       listeners_count[name, port] += 1
+               end
+
+               interfac.registered = true
+       end
+
+       # TODO close listener
+end
+
+redef class Interface
+       # Has `self` been registered by `listen_on`?
+       private var registered = false
+end
+
+redef class Interfaces
+       redef fun add(e)
+       do
+               super
+               if vh.server_config != null then sys.listen_on(e, vh.server_config.factory)
+       end
+
+       # TODO remove
+end
+
+redef class VirtualHosts
+       redef fun add(e)
+       do
+               super
+               for i in e.interfaces do sys.listen_on(i, config.factory)
+       end
+
+       # TODO remove
+end
diff --git a/lib/nitcorn/server_config.nit b/lib/nitcorn/server_config.nit
new file mode 100644 (file)
index 0000000..2decb16
--- /dev/null
@@ -0,0 +1,134 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Classes and services to configure the server
+#
+# The classes of interest are `VirtualHost`, `Interface`, `Route` and `Action`
+module server_config
+
+# Server instance configuration
+class ServerConfig
+       # Virtual hosts list
+       var virtual_hosts = new VirtualHosts(self)
+
+       # TODO implement serialization or something like that
+end
+
+# A `VirtualHost` configuration
+class VirtualHost
+       # Back reference to the associated server configuration
+       var server_config: nullable ServerConfig = null
+
+       # Interfaces on which `self` is active
+       var interfaces = new Interfaces(self)
+
+       # Routes and thus `Action`s active on `self`
+       var routes = new Routes(self)
+
+       # Create a virtual host from interfaces as strings
+       init(interfaces: String ...)
+       do
+               for i in interfaces do self.interfaces.add_from_string i
+       end
+end
+
+# An interface composed of a `name`:`port`
+class Interface
+       # Name of this interface (such as "localhost", "example.org", etc.)
+       var name: String
+
+       # The port to open
+       var port: Int
+
+       redef fun to_s do return "{name}:{port}"
+end
+
+# A route to an `Action` according to a `path`
+class Route
+       # Path to this action present in the URI
+       var path: nullable String
+
+       # `Action` to activate when this route is traveled
+       var handler: Action
+end
+
+# Action executed to answer a request
+abstract class Action
+end
+
+### Intelligent lists ###
+
+# A list of interfaces with dynamic port listeners
+class Interfaces
+       super Array[Interface]
+
+       # Back reference to the associtated `VirtualHost`
+       var vh: VirtualHost
+
+       # Add an `Interface` described by `text` formatted as `interface.name.com:port`
+       fun add_from_string(text: String)
+       do
+               assert text.chars.count(':') <= 1
+
+               var words = text.split(':')
+               var name = words[0]
+               var port
+               if words.length > 1 then
+                       port = words[1].to_i
+               else port = 80
+
+               add new Interface(name, port)
+       end
+end
+
+# A list of virtual hosts with dynamic port listeners
+class VirtualHosts
+       super Array[VirtualHost]
+
+       # Back reference to the server config
+       var config: ServerConfig
+
+       redef fun add(e)
+       do
+               super
+
+               e.server_config = config
+       end
+end
+
+# A list of routes with the search method `[]`
+class Routes
+       # Back reference to the config of the virtual host
+       var config: VirtualHost
+
+       private var array = new Array[Route]
+
+       # Add `e` to `self`
+       fun add(e: Route) do array.add e
+
+       # Remove `e` from `self`
+       fun remove(e: Route) do array.remove e
+
+       # Get the first `Route` than has `key` as prefix to its path
+       fun [](key: String): nullable Route
+       do
+               for route in array do
+                       var path = route.path
+                       if path == null or key.has_prefix(path) then return route
+               end
+               return null
+       end
+end
diff --git a/lib/nitcorn/sessions.nit b/lib/nitcorn/sessions.nit
new file mode 100644 (file)
index 0000000..31bae5d
--- /dev/null
@@ -0,0 +1,109 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Automated session management
+#
+# When parsing a request, this module associate a pre-existing session
+# to the request if there is one. It will also send the required cookie
+# with the response if a session has been associated to the response object.
+module sessions
+
+import md5
+
+import server_config
+import http_request
+import http_response
+
+# A server side session
+class Session
+
+       # Hashed id used both client and server side to identify this `Session`
+       var id_hash: String
+
+       init
+       do
+               self.id_hash = sys.next_session_hash
+               sys.sessions[self.id_hash] = self
+       end
+end
+
+redef class Sys
+       # Active sessions
+       var sessions = new HashMap[String, Session]
+
+       # Get the next session hash available, and increment the session id cache
+       fun next_session_hash: String
+       do
+               var id = next_session_id_cache
+               # On firt evocation, seed the pseudo random number generator
+               if id == null then
+                       srand
+                       id = 1000000.rand
+               end
+
+               next_session_id_cache = id + 1
+
+               return id.to_id_hash
+       end
+
+       private var next_session_id_cache: nullable Int = null
+
+       # Salt used to hash the session id
+       protected var session_salt = "Default unitcorn session salt"
+end
+
+redef class Int
+       # Salt and hash and id to use as `Session.id_hash`
+       private fun to_id_hash: String do return (self.to_s+sys.session_salt).md5
+end
+
+redef class HttpResponse
+       # A `Session` to associate with a response
+       var session: nullable Session = null is writable
+
+       redef fun finalize
+       do
+               super
+
+               var session = self.session
+               if session != null then
+                       header["Set-Cookie"] = "session={session.id_hash}; HttpOnly"
+               end
+       end
+end
+
+redef class HttpRequest
+       # The `Session` associated to this request
+       var session: nullable Session = null
+end
+
+redef class HttpRequestParser
+       redef fun parse_http_request(text)
+       do
+               var request = super
+               if request != null then
+                       if request.cookie.keys.has("session") then
+                               var id_hash = request.cookie["session"]
+
+                               if sys.sessions.keys.has(id_hash) then
+                                       # Restore the session
+                                       request.session = sys.sessions[id_hash]
+                               end
+                       end
+               end
+               return request
+       end
+end