Repository: raksoras/luaw Branch: master Commit: 036601d6a76b Files: 62 Total size: 349.9 KB Directory structure: gitextract_wtxp3faz/ ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── conf/ │ └── server.cfg ├── docs/ │ ├── Contents.md │ ├── Introduction.md │ ├── cmd-line.md │ ├── configuration.md │ ├── first-webapp.md │ ├── getting-started.md │ ├── http-client.md │ ├── logging.md │ ├── proxy-http.md │ ├── resp-obj.md │ ├── second-webapp.md │ ├── template-view.md │ ├── third-webapp.md │ ├── user-threads.md │ └── user-timers.md ├── lib/ │ ├── luapack.lua │ ├── luaw_constants.lua │ ├── luaw_data_structs_lib.lua │ ├── luaw_http.lua │ ├── luaw_init.lua │ ├── luaw_logging.lua │ ├── luaw_scheduler.lua │ ├── luaw_tcp.lua │ ├── luaw_timer.lua │ ├── luaw_utils.lua │ ├── luaw_webapp.lua │ └── unit_testing.lua ├── sample/ │ ├── conf/ │ │ └── server.cfg │ ├── proxy_handler.lua │ └── webapps/ │ └── myapp/ │ ├── handlers/ │ │ ├── handler-address.lua │ │ ├── handler-fileupload-display.lua │ │ ├── handler-fileupload-process.lua │ │ ├── handler-hellouser.lua │ │ ├── handler-helloworld.lua │ │ ├── handler-read-lpack.lua │ │ └── handler-write-lpack.lua │ ├── views/ │ │ └── view-address.lua │ └── web.lua └── src/ ├── Makefile ├── http_parser.c ├── http_parser.h ├── lfs.c ├── lfs.h ├── lua_lpack.c ├── lua_lpack.h ├── luaw_common.c ├── luaw_common.h ├── luaw_http_parser.c ├── luaw_http_parser.h ├── luaw_logging.c ├── luaw_logging.h ├── luaw_server.c ├── luaw_tcp.c ├── luaw_tcp.h ├── luaw_timer.c └── luaw_timer.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ **/*.o **/*.so **/*.log ================================================ FILE: .gitmodules ================================================ [submodule "deps/libuv"] path = deps/libuv url = https://github.com/libuv/libuv.git [submodule "deps/luajit-2.0"] path = deps/luajit-2.0 url = http://luajit.org/git/luajit-2.0.git [submodule "deps/lua-PUC-Rio"] path = deps/lua-PUC-Rio url = https://github.com/lua/lua.git ================================================ FILE: LICENSE ================================================ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # Makefile for building Luaw # == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT ======================= export UVDIR= deps/libuv export UVLIB= deps/libuv/.libs/libuv.a ifeq ($(LUAVM),luajit) export LUADIR= deps/luajit-2.0 export LUALIB= deps/luajit-2.0/src/libluajit.a export OSXLDFLAGS= "-Wl,-pagezero_size,10000 -Wl,-image_base,100000000" else export LUADIR= deps/lua-PUC-Rio export LUALIB= deps/lua-PUC-Rio/src/liblua.a export OSXLDFLAGS= endif # == END OF USER SETTINGS -- NO NEED TO CHANGE ANYTHING BELOW THIS LINE ======= # Supported platforms PLATS= aix ansi bsd freebsd generic linux macosx mingw posix solaris ALL= all # Targets start here. all: @echo "Please do 'make PLATFORM' where PLATFORM is one of these:" @echo " $(PLATS)" $(UVLIB): $(UVDIR)/Makefile $(MAKE) -C $(UVDIR) $(UVDIR)/Makefile: $(UVDIR)/configure cd $(UVDIR) && ./configure $(UVDIR)/configure: $(UVDIR)/autogen.sh cd $(UVDIR) && sh autogen.sh $(LUADIR)/src/libluajit.a: $(MAKE) -C $(LUADIR) $(LUADIR)/src/liblua.a: $(MAKE) -C $(LUADIR) $(TARGET) luaw: $(MAKE) -C src $(ALL) SYSLDFLAGS=$(SYSLDFLAGS) CC=$(CC) # Convenience targets for popular platforms aix: TARGET= aix aix: $(UVLIB) $(LUALIB) $(MAKE) -C src $(ALL) CC="xlc" CFLAGS="-O2" SYSLIBS="-ldl" SYSLDFLAGS="-brtl -bexpall" ansi: TARGET= ansi ansi: $(UVLIB) $(LUALIB) $(MAKE) -C src $(ALL) bsd: TARGET= bsd bsd: $(UVLIB) $(LUALIB) $(MAKE) -C src $(ALL) SYSLIBS="-Wl,-E" freebsd: TARGET= freebsd freebsd: $(UVLIB) $(LUALIB) $(MAKE) -C src $(ALL) SYSLIBS="-Wl,-E" linux: TARGET= linux linux: SYSLIBS= -Wl,-E -ldl linux: $(UVLIB) $(LUALIB) $(MAKE) -C src $(ALL) SYSLIBS="-lrt -Wl,-E -ldl" macosx: TARGET= macosx macosx: $(UVLIB) $(LUALIB) $(MAKE) -C src $(ALL) CC="cc" SYSLDFLAGS=$(OSXLDFLAGS) posix: TARGET= posix posix: $(UVLIB) $(LUALIB) $(MAKE) -C src $(ALL) solaris: TARGET= solaris solaris: $(UVLIB) $(LUALIB) $(MAKE) -C src $(ALL) SYSLIBS="-ldl" #build objects management install: $(MAKE) -C src install install-sample: $(MAKE) -C src install-sample uninstall: $(MAKE) -C src uninstall clean: $(UVDIR)/Makefile $(MAKE) -C deps/luajit-2.0 clean $(MAKE) -C deps/lua-PUC-Rio clean $(MAKE) -C $(UVDIR) distclean $(MAKE) -C src clean # list targets that do not create files (but not all makes understand .PHONY) .PHONY: all check_plat $(LUALIB) $(PLATS) luaw install uninstall clean $(LUADIR)/src/libluajit.a $(LUADIR)/src/liblua.a ================================================ FILE: README.md ================================================ Luaw - Lua meets Node.js ======================== *** Luaw stands for "Lua web server". It also matches abbreviation for an air traffic controller command "Line Up And Wait" that closely resembles the way it handles multiple requests using event loop :) Luaw is an event driven, non blocking IO based HTTP application server inspired by Node.js. It uses Node.js's excellent async library libuv to do non-blocking IO but instead of Javascript it uses [Lua](http://www.lua.org/) as its primary language for application development. Luaw takes advantage of Lua's first class coroutine support to avoid [callback spaghetti problem](http://callbackhell.com/). This makes writing async IO code as straight forward as writing sequential code while all the heavy-lifting of application state management is transparently handled by Lua coroutines. This mean a Luaw application developer gets best of both worlds - [scale](http://www.kegel.com/c10k.html) of event driven IO and code simplicity of blocking IO. ##Features 1. Full HTTP 1.1 support with persistent/keep-alive connections and HTTP pipelining with configurable connect and read timeouts. 2. Two ways to read and parse incoming HTTP requests, - Reading whole request in memory and then parsing it which is suitable for majority of applications. - Reading request as a stream and parsing it as it arrives in parts to minimize latency and server memory footprint for high traffic, high performance applications like HTTP proxies or HTTP load balancers. 3. Similarly on the output side it supports, - Buffering entire content in memory before writing it out to client with the correct "Content-Length" header, or - Streaming response continuously as it is generated using HTTP 1.1 chunked encoding to keep server memory pressure minimum. 4. Fully asynchronous DNS and HTTP client for making remote HTTP calls from server 5. [Sinatra](http://www.sinatrarb.com/) like web application framework that supports mapping URLs to REST resources with full support for path and query parameters. 6. Luaw template views for server side dynamic content generation . Luaw template views use streaming output with chunked encoding described above to eliminate need for huge server side memory buffers to buffer output generated by the templates. 7. Support for spawning user threads that can hook into Luaw's async IO machinery for calling multiple backend services from Luaw server in parallel. 8. Support for user timers for implementing periodic cron like jobs inside Luaw server. 9. Log4j like logging framework with configurable log levels, log file size limits and automatic log rotation that integrates with syslog out of the box. 10. [MessagePack](http://msgpack.org/) like library to efficiently serialize/deserialize arbitrarily complex data into compact and portable binary format for remote web service calls. 11. Built in multipart file upload support. ##How To Build *** 1. Get necessary build tools. libuv, one of the Luaw dependencies uses autotools, autoconf and libtoolize in its build system. If your machine is not already setup with these, you can use following steps to get these tools. sudo apt-get install make sudo apt-get install autotools-dev sudo apt-get install autoconf sudo apt-get install build-essential libtool 2. Clone Luaw repository git clone --recursive https://github.com/raksoras/luaw.git 3. Build Luaw to use standard [PUC-Rio Lua VM](http://www.lua.org/) cd luaw make linux or, build Luaw to use [LuaJit VM](http://luajit.org/) cd luaw make LUAVM=luajit linux While building on mac OS use target "macosx". For example cd luaw make LUAVM=luajit macosx 4. Install Luaw binary - luaw_server - in directory of your choice. We will use `~/luawsample` in all our examples going forward as a directory of choice for Luaw installation make INSTALL_ROOT=~/luawsample install To install binaries along with the sample app provided make INSTALL_ROOT=~/luawsample install-sample ##Luaw directory structure In the tree diagram below `~/luawsample` is a directory that you chose in the `make intsall` step above. It will act as a root for Luaw server's directory structure. The `make install` step will create following directory structure under `~/luawsample` ``` ~/luawsample | | +--- bin Directory that holds Luaw server binary we built | along with all necessary Lua libraries | +--- conf Directory for server configuration | | │   +--- server.cfg Sample server configuration file, to be used as a starting point | +--- lib Directory to install any third party or user supplied Lua | libraries that application may depend on. | +--- logs Directory for server logs | +--- webapps Directory to install Luaw webapps ``` ##License Luaw uses [MIT license](http://opensource.org/licenses/mit-license.html) for all parts of Luaw that are not externally maintained libraries like libuv. ## Documentation Please refer to the [project wiki](https://github.com/raksoras/luaw/wiki) for documentation regarding how to build, configure, start and program using Luaw. ## Getting in touch Please post your questions/comments on Google Group [Luaw](https://groups.google.com/forum/#!forum/luaw) and follow [@raksoras](https://twitter.com/raksoras) on Twitter ================================================ FILE: conf/server.cfg ================================================ luaw_server_config = { server_ip = "0.0.0.0", server_port = 7001, connect_timeout = 4000, read_timeout = 8000, write_timeout = 8000 } luaw_log_config = { log_dir = "./logs", log_file_basename = "luaw-log", log_file_size_limit = 1024*1024, log_file_count_limit = 9, log_filename_timestamp_format = '%Y%m%d', log_lines_buffer_count = 16, syslog_server = "127.0.0.1", syslog_port = 514, } luaw_webapp_config = { base_dir = "./webapps" } ================================================ FILE: docs/Contents.md ================================================ #Contents 1. Introduction 2. Getting started: building and installing Luaw 3. Luaw configuration 4. Your first Luaw webapp - "Hello world!" 5. Your second Luaw webapp - "Hello ``!" 6. Luaw template views 7. Your third webapp - with template views 8. Using Luaw logging framework 9. Using Luaw response object 10. Using Luaw async HTTP Client 11. Using Luaw threads and background jobs 12. Using Luaw timers 13. Using custom HTTP handler for HTTP stream parsing 14. Using custom scripts on command line at Luaw start up ================================================ FILE: docs/Introduction.md ================================================ #Introduction Luaw stands for "Lua web server". It also matches abbreviation for an air traffic controller command "line up and wait" that closely resembles the way it handles multiple requests using event loop :) Luaw is an event driven, non blocking IO based HTTP application server inspired by node.js. It uses node.js's excellent async library libuv to do non-blocking IO but instead of Javascript it uses [Lua](http://www.lua.org/) as its language for application development. Luaw takes advantage of Lua's first class coroutine support to avoid [callback spaghetti problem](http://callbackhell.com/). This makes writing async IO code as simple as writing sequential code while all the heavy-lifting of application state machine management that is usually associated with async IO is transparently handled by Lua coroutines. This mean a Luaw application developer gets best of both worlds - [scale](http://www.kegel.com/c10k.html) of event driven IO and simplicity of blocking IO like code. ##Features 1. Full HTTP 1.1 support with persistent/keep-alive connections and HTTP pipelining with configurable connect and read timeouts. 2. Two ways to read and parse incoming HTTP requests, - Reading whole request in memory and then parsing it which is suitable for majority of applications. - Reading request as a stream and parsing it as it arrives in parts to minimize latency and server memory footprint for high traffic, high performance applications like HTTP proxies or HTTP load balancers. 3. Similarly on the output side it supports, - Buffering entire content in memory before writing it out to client with the correct "Content-Length" header. - Streaming response continuously as it is generated using HTTP 1.1 chunked encoding to keep server memory pressure minimum. 4. Fully asynchronous DNS and HTTP client for making remote HTTP calls from server 5. [Sinatra](http://www.sinatrarb.com/) like web application framework that supports mapping URLs to REST resources with full support for path and query parameters. 6. Luaw template views for server side dynamic content generation . Luaw template views use streaming output with chunked encoding described above to eliminate need for huge server side memory buffers to buffer output generated by templates. 7. Support for spawning user threads that can hook into Luaw's async IO machinery for calling multiple backend services from Luaw in parallel. 8. Support for user timers for implementing periodic cron like jobs inside Luaw server. 9. Log4j like logging framework with configurable log levels, log file size limits and automatic log rotation that integrates with syslog out of the box. 10. [MessagePack](http://msgpack.org/) like library to efficiently serialize/deserialize arbitrarily complex data into compact and portable binary format for remote web service calls. ================================================ FILE: docs/cmd-line.md ================================================ #14. Advanced Topic II - Using custom scripts on command line at start up In the last topic we saw that we can specify any custom Lua script(s) on the command line after the server configuration file at start up. All these script are executed sequentially in order by Luaw using the same Lua VM and global environment. These scripts are executed after all the core infrastructure machinery of Luaw is initialized. This means these custom scripts have full access to initialized Luaw container before it starts to server any traffic. This is a very flexible and powerful trick to customize Luaw server and modify the global environment in which all subsequent request handlers will run. We have already used this trick to override Luaw's default HTTP request handler with our own custom request handler optimized for proxying network traffic. You can use this trick to do many essential, common tasks at server start up time. For example, 1. Loading any in memory caches required by application in Luaw's memory 2. Start any crontab like tasks that you would like to be run periodically inside your Luaw server. To do this you would typically spawn a new user thread per task and use a timer:sleep() method to sleep for desired time and then execute whichever action you want in a forever loop 3. Set up sandboxes in which to execute certain request or resource handlers. ================================================ FILE: docs/configuration.md ================================================ #3. Configuring Luaw Before we can write our first Luaw webapp we need to configure our Luaw server with some basic settings -- TCP port on which Luaw server will listen for incoming HTTP connections, for example. These settings are specified in a text file which is named server.cfg by convention and is put under `luaw_root_dir/conf` directory. This file is Luaw's counterpart of Apache web server's httpd.conf or Tomcat's server.xml file. All Luaw configuration files use [Lua object notation syntax](http://www.lua.org/pil/10.1.html#DataDesc) and server.cfg is no exception to this rule. Like Tomcat, Luaw allows deploying multiple web apps in a single Luaw server. Settings configured in server.cfg apply to the whole server, that is, they apply to all webapps that are deployed in that server. Settings specific to each webapps are configured using "web.lua" file per webapp. `make INSTALL_ROOT=luaw_root_dir install` step from the last chapter should have created the standard directory structure for you and put sample server.cfg file under `luaw_root_dir/conf` directory. You can use this server.cfg as a starting point for defining your configuration. Open your conf/server.cfg and take a look. ##Server configuration Here is a sample server.cfg ```lua luaw_server_config = { server_ip = "0.0.0.0", server_port = 7001, connect_timeout = 4000, read_timeout = 8000, write_timeout = 8000 } luaw_log_config = { log_dir = "/apps/luaw/sample/log", log_file_basename = "luaw-log", log_file_size_limit = 1024*1024, log_file_count_limit = 9, log_filename_timestamp_format = '%Y%m%d', log_lines_buffer_count = 16, syslog_server = "127.0.0.1", syslog_port = 514, } luaw_webapp_config = { base_dir = "/apps/luaw/sample/webapps" } ``` luaw_server_config section specifies listening port and read/connection timeout defaults for TCP socket connections. "server_ip" setting's value "0.0.0.0" tells server to accept connections coming in on any of the host's ip addresses. Some hosts have more than one IP address assigned to them. In such case "server_ip" can be used to restrict Luaw server to accept incoming connections on only one of the multiple IP addresses of the host. luaw_log_config section sets up parameters for Luaw's log4j like logging subsystem - log file name pattern, size limit for a single log file after which Luaw should open new log file, how many of such past log files to keep around (log rotation) etc. Luaw logging framework can send messages to syslog daemon as well and this section can be used to specify target syslog server's ip address and port. Finally, luaw_webapp_config section specifies location of directory that houses all the webapps that this Luaw server will load and run. By convention this directory is named "webapps" and is placed directly under Luaw server's root folder but you can place it anywhere you like using this section, should your build/deploy procedure requires you to choose another location. ## webapp configuration Like Tomcat, Luaw allows deploying multiple webapps in a single Luaw server. These webapps are deployed under `luaw_root_dir/webapps`. Here is a sample layout for a Luaw server that has two webapps - `myapp1` and `myapp2` - deployed in it: ``` luaw_root_dir | +--- bin | +--- conf | | │   +--- server.cfg | +--- lib | +--- logs | +--- webapps | +--- myapp1 | | | +---web.lua | +--- myapp2 | +---web.lua ``` Each webapp contains file named web.lua that specifies settings specific to that particular webapp. The same directory (`/myapp1` and `/myapp2` in the example above) also contains Lua code for the webapp - REST handlers, views etc. We will visit application code in the next chapter. In this chapter we will focus on just the configuration piece. ##Sample web.lua: ```lua luaw_webapp = { resourcePattern = "handler%-.*%.lua", views = { "user/address-view.lua", "account/account-view.lua" } } Luaw.logging.file { name = "root", level = Luaw.logging.ERROR, } Luaw.logging.file { name = "com.luaw", level = Luaw.logging.INFO, } Luaw.logging.syslog { name = "com.luaw", level = Luaw.logging.WARNING, } ``` luaw_webapp section specifies resources (request handlers) and views (templates) that make up the web application. These can be specified in two different ways: 1. **Using pattern **: You can use configuration elements *resourcePattern* and *viewPattern* to specify name pattern for request handlers and views. Luaw will traverse all directories under current webapp recursively to load all files that match these patterns. The patterns are specified using standard [Lua regular expressions](http://www.lua.org/pil/20.2.html). These are somewhat different than usual POSIX or PERL regular expressions so be sure to read the documentation linked above before you use them. 2. **Listing them one by one by exact name and path **: You can also use configuration elements *resources* and *views* to list exact resources and views by name to load them. Each entry should contain a path that is relative to the root webapp directory (**myapp1** and **myapp2** in the example above) ending in the filename of the file to load. You can mix and match both these approach. For example you can use *resourcePattern* to specify resources and *views* for specifying exact list of views. You can even use both the ways together. That is, you can use both *resourcePattern* and *resources* in the same section and Luaw will load all the files under the given webapp's folder that either match the *resourcePattern* or match the path and file name from the *resources* list (union operation). This could be useful if most of your resource files follow certain naming convention or a pattern but you also have few one off files that don't follow those conventions that you'd like to load nonetheless. Rest of the file specifies logging level and logging target (file vs syslog) for different Luaw packages. Logging settings in web.lua specify log levels that are specific to that webapp while overall logging settings like log file name and size limit are determined by server.cfg settings at a global server level. ================================================ FILE: docs/first-webapp.md ================================================ #4. Your First Luaw Webapp - "Hello world!" Now that we are familiar with Luaw's directory layout and configuration, we are ready to write our first webapp - "Hello world" but of course. Luaw comes equipped with [Ruby's Sinatra](http://www.sinatrarb.com/) like web framework that allows mapping URLs to request handlers (routes in Sinatra's terminology). It has full support for REST path and query parameters. ##Writing Luaw Request Handler 1. Switch to directory `luaw_root_dir/webapps` that was created in chapter "Getting started" and create a directory called `myapp` under it. This is our first webapp. cd luaw_root_dir/webapps mkdir myapp cd myapp 2. create a filed called web.lua in `luaw_root_dir/webapps/myapp` and put following content in it ```lua luaw_webapp = { resourcePattern = "handler%-.*%.lua", } ``` This is a bare minimum webapp configuration that basically tells Luaw to load any file matching Lua regex pattern "handler%-.*%.lua" as a URL (or in REST terminology Resource) handler. 3. Now we will write our first resource handler. create a directory `handlers` under `luaw_root_dir/webapps/myapp` and create a file named `handler-helloworld.lua` under it. The choice of the directory name "handlers" is purely arbitrary. All that matters is that handler's file name matches the pattern `handler%-.*%.lua` that we have specified in web.lua. Luaw will traverse all folders under `luaw_root_dir/webapps/myapp` looking for handler files to load that match the pattern. This means we could have placed handler-helloworld.lua directly under luaw_root_dir/webapps/myapp and it would have still worked. It's probably a better practice to put them in their own directory like "handlers" from the point of view of clean code organization though. Next put following code in "handler-helloworld.lua": ```lua GET 'helloworld' { function(req, resp, pathParams) return "Hello World!" end } ``` In the code above GET identifies the HTTP method this handler will service. Other methods available are POST, PUT, DELETE, HEAD, OPTIONS, TRACE and CONNECT corresponding to respective HTTP methods. There is also a catch-all, uber method called SERVICE that you can use in case you want to handle any HTTP request irrespective of its method. the string 'helloworld' specifies the URL path this handler will match. It is analogus to Sinatra's route. It means this handler will be invoked for all GET methods made on `/myapp/helloworld` URL. Finally, the function in the above example is the actual code run whenever `/myapp/helloworld` is requested. The function is passed incoming HTTP request, HTTP response to write to and any REST path parameters defined on the resources. We will see how to used all three of these objects in subsequent chapters. For now, we just want to follow the age old rite of passage and say "Hello World!" to the browser. Simply returning the string "Hello World!" from our function is sufficient to achieve this. Later we will see more sophisticated ways of forming and returning response using response object and Luaw template views. export LD_LIBRARY_PATH=/usr/local/lib/ 4. Now we are ready to start the server. To do this switch to `luaw_root_dir` and run Luaw server like this: cd luaw_root_dir ./bin/luaw_server ./conf/server.cfg First argument to the luaw_server is always the server configuration file. If you have followed all the steps so far correctly, you should see following output in your console window: ``` ******************** Starting webapp myapp ******************************* .Loading resource ./webapps/myapp/handlers/handler-helloworld.lua #Loaded total 1 resources #Compiled total 0 views *********************** Webapp myapp started **************************** starting server on port 7001 ... ``` Now point your browser to http://localhost:7001/myapp/helloworld and greet the brave new world! Congratulations, you have successfully written your first Luaw webapp! ================================================ FILE: docs/getting-started.md ================================================ #2. Getting started Let's build Luaw from its sources. Luaw depends on, 1. Lua 5.2(+) 2. libuv (v1.0.0 ) We will first build these dependencies from their sources and then finally build Luaw itself. To build these artifacts you would need [Git](http://git-scm.com/), [Make](http://www.gnu.org/software/make/) and [autotools](http://www.gnu.org/software/automake/manual/html_node/Autotools-Introduction.html) setup on your machine. ##Building Lua 5.2 Lua sources can be downloaded from [here](http://www.lua.org/download.html). Here are the steps to download Lua 5.2 sources and build it for Linux: curl -R -O http://www.lua.org/ftp/lua-5.2.3.tar.gz tar zxf lua-5.2.3.tar.gz cd lua-5.2.3 make linux test sudo make linux install To build for other OSes replace "linux" from the last two make commands with the OS you are building for. For example for when building for Mac OS run, make macosx test sudo make linux install To see what OSes are supported run ./lua-5.2.3/src/Makefile targets ##Building libuv Luaw uses node.js library libuv to do asynchronous, event based IO in a portable, cross platform manner. To build libuv: 1. first clone libuv repository git clone https://github.com/joyent/libuv.git 2. Checkout latest stable release of libuv from the cloned local repository. As of this writing the latest stable release is v1.0.0 and Luaw is verified to compile and run successfully with this release of libuv. cd libuv git checkout tags/v1.0.0 3. Build libuv. This may require you to install autotools. Detailed instructions are [here](https://github.com/joyent/libuv#build-instructions) sh autogen.sh ./configure make make check sudo make install ## Building Luaw With all dependencies built, now we are ready to build Luaw itself. 1. Clone Luaw repository git clone https://github.com/raksoras/luaw.git 2. Build Luaw cd luaw/src make linux 3. Install Luaw binary - luaw_server - in directory of your choice make INSTALL_ROOT= install 4. Note: On Mac running Yosemite version of Mac OS you may have to run, make SYSCFLAGS=-I/usr/local/include SYSLDFLAGS=-L/usr/local/lib macosx make INSTALL_ROOT= install ##Luaw directory structure In the tree diagram below `luaw_root_dir` is a directory that you chose in the `make intsall` step above. It will act as a root for Luaw server's directory structure. The `make install` step will create following directory structure under `luaw_root_dir` ``` luaw_root_dir | | +--- bin Directory that holds Luaw server binary we built | along with all necessary Lua libraries | +--- conf Directory for server configuration | | │   +--- server.cfg Sample server configuration file, to be used as a starting point | +--- lib Directory to install any third party or user supplied Lua | libraries that application may depend on. | +--- logs Directory for server logs | +--- webapps Directory to install Luaw webapps ``` This directory structure and its usage is further explained in the next chapter "Configuring Luaw" ================================================ FILE: docs/http-client.md ================================================ #10. Async HTTP Client Luaw comes equipped with curl like async HTTP client. It is fully async, in that both DNS lookup for the hostname as well as actual connect/read/write on the socket are done in a non blocking fashion. Due to this you can use this client safely in your Luaw webapp, from your request thread to make HTTP requests to other backend servers. Luaw will transparently suspend your running request thread (Lua coroutine) when the HTTP client is waiting on DNS lookup, connect, read or write. The Luaw HTTP client is modeled by two objects: clientHttpRequest and clientHttpResponse. Here is a small example of Luaw's HTTP client's usage: ```lua -- set up HTTP request local clientReq = Luaw.newClientHttpRequest() clientReq.hostName = "www.google.com" -- OR alternatively, clientReq.hostIP = "74.125.25.106" clientReq.method = 'GET' clientReq.url = "/" clientReq.headers = { Host = "www.google.com" } -- OR alternatively clientReq:addHeader("Host", "www.google.com") -- execute the HTTP request and read the response back. local clientResp = clientReq:execute() -- Get the respone headers and body from the client response object returned local respBody = clientResp.body local respHeaders = clientResp.headers ``` In fact, Luaw's built in HTTP client allows even more fine grained control over various stages of HTTP request execution and parsing of the HTTP response received from the server, similar to what we saw in the chapter "Advanced Topic I - Using Response Object" which was about server's HTTP response. We learn will how to use some of these methods in the last chapter where we put together all the things we have learned so far to develop a streaming request/response handler for a high performance proxy web server. ================================================ FILE: docs/logging.md ================================================ #8. Luaw logging framework Luaw comes equipped with logging framework that's modeled after Java's popular log4j logging framework. Like log4j, it allows different destinations for logs (either file system or syslog) and different log levels per package to allow enabling/disabling logging by setting runtime properties Luaw's logging is configured at two levels ##Logging configuration for the whole server Configuration related to log destinations is configured in `conf/server.cfg` and applies to all web applications deployed in that server. luaw_log_config section contains this configuration in server.cfg ```lua luaw_log_config = { log_dir = "/apps/luaw/sample/log", log_file_basename = "luaw-log", log_filename_timestamp_format = '%Y%m%d', log_file_size_limit = 1024*1024, log_file_count_limit = 9, log_lines_buffer_count = 16, syslog_server = "127.0.0.1", syslog_port = 514 } ``` Example above configures, 1. Log directory where the files should go: /apps/luaw/sample/log 2. Log file base name: luaw-log 3. [Timestamp format](http://www.lua.org/pil/22.1.html) used along with log file base name to generate full log file name for versioned log files: %Y%m%d 4. File size limit for an individual log file: 1MB 5. Total number of log files to keep: 9 6. How many log lines to buffer in memory before flushing them to log file: 16 7. syslog server address: 127.0.0.1 8. syslog server port: 514 You could omit either file system related configuration or syslog configuration but at least one must be present to use Luaw logging. ##Logging configuration per webapp Configuration related to individual webapps' logging is configured in `web.lua` config file of each webapp. Here are some example entries from sample "web.lua": ```lua Luaw.logging.file { name = "root", level = Luaw.logging.ERROR } ``` Above entry configures root level logging ("root" is a special word) for the entire webapp that applies to all loggers used by the webapp by default unless overridden by more specific logger. It configures all logging to go to log file - whose destination and name is configured in server.cfg - and states only logs with log level ERROR and above should be logged. ```lua Luaw.logging.file { name = "com.myapp", level = Luaw.logging.INFO } ``` Overrides default ("root") logging for the logger "com.myapp" and sets its level to INFO which means for any logger with name starting with "com.myapp", log lines with the log level INFO and above will get logged to the file instead of just ERROR as dictated by the default (root) configuration above. "com.myapp" is a logger name. You can use any name separated by periods. Each part of the name delimited by a period forms logger hierarchy just like log4j. For example, the logger named "com" is a parent to logger name "com.myapp" and any logging configuration defined for "com" is inherited by "com.myapp" unless specififcally overriden on "com.myapp". This makes it easy to fine tune logging levels of different parts of code using loggers arranged in a logical hierarchy. To actually log a line you use code like following: ```lua local log = require("luaw_logging") local logger = log.getLogger("com.myapp") logger.error("some error occurred") logger.warning("this is a warning") logger.info("some information") logger.debug("lowest level debug") ``` Here are different log levels and Luaw logger functions corresponding to them in a decreasing order of severity: |Log Level | Logging function | |-------------------------------- | EMERGENCY | logger.emergency() | | ALERT | logger.alert() | | CRITICAL | logger.critical() | | ERROR | logger.error() | | WARNING | logger.warning() | | NOTICE | logger.notice() | | INFO | logger.info() | | DEBUG | logger.debug() | All these functions also have a counterpart ending in "f" - `logger.errorf()`, `logger.warningf()`, `logger.debugf()` etc. - that take a [lua format string](http://lua-users.org/wiki/StringLibraryTutorial) followed by variable number of arguments that are used to substitute place holderes in the format string like below ```lua logger.infof("User name %s, User Id %d", name, id) ``` This form is useful to avoid unnecessary string concatenation beforehand when the configured logger level may actually end up filtering the log line anyways. Finally, here is an example of configuring syslog as a log destination for logger "com.myapp.system" and setting it to the log level WARNING in web.lua: ```lua Luaw.logging.syslog { name = "com.myapp.system", level = Luaw.logging.WARNING } ``` syslog sever and port to use is specified in server.cfg ================================================ FILE: docs/proxy-http.md ================================================ #13. custom HTTP handler for proxying request So far we have used default HTTP requests handler that ships with Luaw in all our examples. For most purposes it is indeed sufficient, even ideal HTTP handler to use. It handles all low level details of HTTP protocol parsing and routes incoming requests to REST resource by matching URL to user specified matching rules. However, if you need it, Luaw also allows you to specify your own custom HTTP request handler - which is top level HTTP request router really comparable to Strut's main servlet or Spring MVC servlet counterpart in Java world - which will replace Luaw's default request handler. We will use this Luaw facility to develop a toy HTTP buffering reverse proxy server in this section. A typical reverse proxy server accepts a HTTP request from a client, inspects its contents and then forwards it to one of many backend HTTP servers it is balancing load for depending upon the results of the request content inspection. On the return path it receives the response generated by the backend server and forwards it to the connected client. Buffering reverse proxy adds, well, buffering to the standard reverse proxy functionality. It buffers incoming HTTP client request till it is complete before it sends it to one of the backend servers. Similarly on the return path it buffers full response generated by the backend server before it starts returning it back to the original client. This buffering is important in order to defend backend servers' limited resoucres against slow client. Without buffering both consumption of input HTTP request and sending of HTTP response will proceed at "line speed" dictated by the slow client which will tie backend server's resources for a long time. ``` +------+ ----HTTP request---> +-------------+ --- Proxy HTTP request ---> +-------+ |Client| |Reverse Proxy| |Backend| +------+ <---HTTP response--- +-------------+ <-- Proxy HTTP response --- +-------+ ``` Below is the code for a toy buffering reverse proxy with inline comments explaining what's going on. Our toy proxy server uses a protocol where by client sends the backend host and URL it wants to connect through the proxy server in its HTTP request's "proxy-host" and "proxy-url" headers respectively. If any of these headers are missing our proxy server responds with 400 error. It also proxies only HTTP GET requests but its easy to see how it can be modified to proxy other HTTP methods too. ```lua --[[ Luaw allows you to replace it's default MVC/REST request handler with your own custom HTTP request handler implementation. To override the default HTTP request handler just set Luaw object's request_handler property to your custom Lua function. This function is passed in a low level connection object for each incoming request instead of the normal request and response objects. The function is called on its own separate Luaw coroutine for each HTTP request so you don't have to worry about multithreaded access to same state inside the function. ]] Luaw.request_handler = function(conn) conn:startReading() -- loop to support HTTP 1.1 persistent (keep-alive) connections while true do local req = Luaw.newServerHttpRequest(conn) local resp = Luaw.newServerHttpResponse(conn) -- read and parse full request local eof = req:readFull() if (eof) then conn:close() return "connection reset by peer" end local reqHeaders = req.headers local beHost = reqHeaders['backend-host'] local beURL = reqHeaders['backend-url'] if (beHost and beURL) then local backendReq = Luaw.newClientHttpRequest() backendReq.hostName = beHost backendReq.url = beURL backendReq.method = 'GET' backendReq.headers = { Host = beHost } local status, backendResp = pcall(backendReq.execute, backendReq) if (status) then resp:setStatus(backendResp:getStatus()) resp:appendBody(backendResp:getBody()) local beHeaders = backendResp.headers for k,v in pairs(beHeaders) do if ((k ~= 'Transfer-Encoding')and(k ~= 'Content-Length')) then resp:addHeader(k,v) end end backendResp:close() else resp:setStatus(500) resp:appendBody("connection to backend server failed") end else resp:setStatus(400) resp:appendBody("Request must contain headers backend-host and backend-url") end local status, mesg = pcall(resp.flush, resp) if (not status) then conn:close() return error(mesg) end if (req:shouldCloseConnection() or resp:shouldCloseConnection()) then conn:close() return "connection reset by peer" end end end ``` The last bit of puzzle remaining is how do we actually load this new, shiny custom HTTP handler of ours into Luaw server? To this we use a simple trick that is quite flexible and powerful in practice. So far we have been starting our Luaw server with following command in luaw_roo_dir: ``` ./bin/luaw_server ./conf/server.cfg ``` Where `server.cfg` is a Luaw server configuration file. In reality `luaw_server` binary will read and execute any number of Lua script files specified as series of command line arguments. The very first one is assumed to be server configuration file and is mandatory. Any number - and any kind - of Lua script files can follow the configuration file and are executed by `luaw_server` in the same order as they are specified on the start up commmand line using the same Lua VM and global environment. We can use this handy trick to load any functions we want into Luaw as well as hook up into Luaw's internal machinery using any of the public hook up points that Luaw advertises. So for our case, just create a file called proxy-handler.lua under `luaw_roo_dir/bin` folder and put the above code in it. Then from your command prompt run Luaw like this: ./bin/luaw_server ./conf/server.cfg ./bin/proxy-handler.lua `luaw_server` will run your proxy-handler.lua after it has initialized itself. The script in proxy-handler.lua then takes care of replacing Luaw's default HTTP request handler with the custom one by assigning the custom handler function to `Luaw.request_handler` property. Now test your shiny new proxy server by running following tests: ## Test 1 - Missing required headers $ curl -v http://127.0.0.1:7001/ * Trying 127.0.0.1... * Connected to 127.0.0.1 (127.0.0.1) port 7001 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.37.1 > Host: 127.0.0.1:7001 > Accept: */* > < HTTP/1.1 400 Bad Request < Content-Length: 50 < Headers proxy-host and proxy-url must be present ## Test 2 - With correct headers $ curl -v -H"proxy-host: www.google.com" -H"proxy-url: /" http://127.0.0.1:7001/ * Trying 127.0.0.1... * Connected to 127.0.0.1 (127.0.0.1) port 7001 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.37.1 > Host: 127.0.0.1:7001 > Accept: */* > proxy-host: www.google.com > proxy-url: / > < HTTP/1.1 200 OK < Content-Type: text/html; charset=ISO-8859-1 < Transfer-Encoding: chunked * Server gws is not blacklisted < Server: gws (.. followed by the body of the home page at www.google.com) ================================================ FILE: docs/resp-obj.md ================================================ #9. Response Object So far we have seen two ways to return response body from resource handler: 1. Returning a string from resource handler function which Luaw returns in turn as a whole response body to the client, and 2. Using Luaw template views that generate response body programmatically in a fashion similar to JSP or ASP However, Luaw does offer finer grain control over response generation from resource handler should you need it. Remember the resource handler function is passed in both request and response object like this: ```lua GET '/user/:username' { function(req, resp, pathParams) return "Hello "..pathParams.username.."!" end } ``` Finer grained control over response generation is achieved by invoking various methods on the response object as described below. 1. `resp:setStatus(status)`: You can set HTTP status code to be returned to the client - 200, 404 etc. - using this method 2. `resp:addHeader(name, value)`: You can add arbitrary HTTP headers to the response using this method. All the headers must be added before you start adding body content. 3. `resp:startStreaming()`: Calling this method activates a special [HTTP 1.1 chunked transfer mode](http://en.wikipedia.org/wiki/Chunked_transfer_encoding) which causes Luaw to stream response to the connected client instead of buffering it in memory till end and then sending it in a single shot. In this mode, any body content added to the response is buffered till it reaches a certain , relatively small buffer size threshold - 2K by default whihch is configurable using property "connection_buffer_size" in server.cfg's luaw_server_config section - and then is sent to the client as a HTTP 1.1 compliant body chunk. This means server does not have to buffer the entire response body in its memory to calculate "Content-Length" header value before it can send it to the client. Thus, this mode improves overall server memory footprint and also client's response time to the first byte received. Luaw template views use this mode by default to generate content. HTTP status and all the HTTP headers must be added to response before resp:startStreaming() is called. 3. `resp:appendBody(content)`: You can use this method to add content to the response body in piecemeal fashion. Depending upon whether the response is in default HTTP 1.1 mode or put in HTTP 1.1 chunked transfer mode by calling resp:startStreaming(); the content is either buffered till resp:flush() is called or streamed to the client in HTTP 1.1 chunks whenever buffered content reaches size limit specified by connection_buffer_size. 4. `resp:flush()`: Causes the response to be flushed to the client. In default (HTTP 1.1) mode this causes Luaw to calculate correct "Content-Length" header value for the whole response buffered so far in the memory and then send it to the client along with the "Content-Length" header. In case the response object was put in the HTTP 1.1 chunked transfer mode by calling resp:startStreaming() this causes Luaw to send the last HTTP chunk followed by the terminating chunk as required by the HTTP 1.1 specification. 5. `resp:close()`: Finally, call this method to actually close underlying connection to client and release all the associated resources. ================================================ FILE: docs/second-webapp.md ================================================ #5. Your Second Luaw webapp - "Hello `username`!" Luaw handlers can accept and process HTTP request parameters (query parameters as well as form parameters) using the request object that is passed to the resource handler function. These parameters are available as `req.params`. For example to access HTTP parameter 'username' - either passed as a query parameter like `?username=raksoras` or POSTed as a form field - you can do either `req.params.username` or `req.params['username']` Luaw also supports mapping parts of URL paths to REST path parameters. We will use this method to receive username. Let's say we want to use URL format '/user/raksoras' where raksoras is the input user name. To do this create a new handler named "handler-hellouser.lua" under `luaw_root_dir/webapps/myapp/handlers` that we created in previous chapter and put following code in it: ```lua GET '/user/:username' { function(req, resp, pathParams) return "Hello "..pathParams.username.."!" end } ``` Here colon in `:username` in the URL path `user/:username` identifies it as a REST path parameter. Luaw will parse it from the URL path at runtime and will make it available on the third paramter - pathParams - passed to the handler function. Inside the function you may refer to it as either `pathParams.username` or `pathParams['username']` Now restart your Luaw server and then point your browser to http://localhost:7001/myapp/user/your_name and Luaw should greet you this time in a more personal manner! In fact, the preceding ":" identifies the path parameter as a string path parameter. You can use preceding "#" to identify a path parameter as a numerical path parameter instead and Luaw will automatically parse it as a number. For example, you can change the handler-hellouser.lua code as follows, ```lua GET '/user/:username/#count' { function(req, resp, pathParams) return "Hello "..pathParams.username.."! You are user number "..pathParams.count.." to visit this site." end } ``` and then point your browser to "http://localhost:7001/myapp/user/raksoras/9" to get ``` Hello raksoras! You are user number 9 to visit this site. ``` in your browser window. ================================================ FILE: docs/template-view.md ================================================ #6. Luaw Template Views Luaw comes equipped with a Luaw template views for generating server-side dynamic content. Luaw template views serve the same need as served by solutions like JSP, ASP and PHP. In reality, a Luaw template view is a normal, plain Lua code with little bit of syntactical sugar added for generating HTML/XHTML/XML markup. This allows developer to use full power of Lua - including if-then-else conditional checks, for/while loops, local variables and functions etc. - within the template view while still using concise, readable notation for generating markup in output. Of course this power can be abused by writing complex logic - which should belong to a well defined, separate business logic tier - inside a template view but we trust that you will do no such thing :) Remember with great power comes great responsibility! Without further ado, here is a sample Luaw template view file - view-address.lua: ```lua BEGIN 'html' BEGIN 'head' BEGIN 'title' TEXT 'Address' END 'title' END 'head' BEGIN 'body' BEGIN 'div' {class='address'} BEGIN 'h1' TEXT(model.title) END 'h1' BEGIN 'table' {border="1", margin="1px"} BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'City' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT(model.city) END 'td' END 'tr' if (model.zip == 94086) then BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'County' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'Santa Clara' END 'td' END 'tr' end BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'Zip' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT(model.zip) END 'td' END 'tr' END 'table' END 'div' END 'body' END 'html' ``` Each Luaw template view has access to following implicitly defined variables: `req`, `resp`, `pathParams` and `model` passed from resource handler that invoked the view It also defines three syntax sugar extentions to generate markeup `BEGIN`, `TEXT` and `END`. 1. You use `BEGIN tag_name` to open any HTML or XML tag of type tag_name 2. If the tag has any attributes you follow the `BEGIN tag_name` with set of attributes defined like this `{name1=value1, name2=value2 ...}` 3. You close opne tag with `END tag_name` 4. If you want to emit any content in the body of the response you are generating you can use `TEXT()` to do so. `TEXT()` can take multiple, variable number of arguments of different types. It will include them in the output response in the same order by calling tostring() on each of the arguments. Consequtive arguments are separated by a single space in the response. As a special case if you want to emit a literal string ('Hello world!' for example) you can do so without using parenthesis like this: `TEXT 'Hello world!'`. There is nothing special about this syntax. Lua as a language offers this convenient notation for calling any Lua function with a single, literal string argument. That's it! You can mix your normal Lua code - if/else, loops etc. - along with the markup to be genertated easily. Take a look at the check for zipcode in the sample code above to see an example of this. In the next chapter we will see how to use Luaw template views with resource handlers. **_NOTE:_** You can write resusable templates to generate markup that is common across many pages - web site header and footer, for example - by writing Lua functions that generate this common markup and then simply invoking these where you want to include the markup. This is very similar to JSP tag libraries or other server side include technologies. The only tricky part is making syntax sugar extentions like `BEGIN`, `TEXT` and `END` available to normal Lua functions outside Lua template views. This is actually very easy. Behind the scene these three extentions are really closures bound to current request/response scope. This means you can pass them to any normal Lua functions - even functions defined in separate .lua files that themselves are not Lua template views - like this: ```lua -- Reusable markup generating function AKA tag library function generateHeader(model, BEGIN, TEXT, END) BEGIN 'div' {class='header') --- generate HTML here using BEGIN, TEXT and END END 'div' end ``` ```lua -- Using common markup generating function from Luaw template view local ssi = require("common_markup.lua") -- Just call function to include the markup at write place ssi.generateHeader(model, BEGIN, TEXT, END) BEGIN 'div' {class="body"} --- generate page specific page markup here END 'div' ssi.generateFooter(model, BEGIN, TEXT, END) ``` ================================================ FILE: docs/third-webapp.md ================================================ #7. Your third webapp - with Luaw template view In this chapter we will put together all the pieces we have learned about so far - resource handler reading REST path parameters + Luaw template view - to build a toy but nevertheless complete MVC solution with Luaw. ##Luaw template view Create a new directory `views` under `luaw_root_dir/webapps/myapp` and add a file named "view-address.lua" to the `views` directory, containing following piece of Luaw template view code from the last chapter: ```lua BEGIN 'html' BEGIN 'head' BEGIN 'title' TEXT 'Address' END 'title' END 'head' BEGIN 'body' BEGIN 'div' {class='address'} BEGIN 'h1' TEXT(model.title) END 'h1' BEGIN 'table' {border="1", margin="1px"} BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'City' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT(model.city) END 'td' END 'tr' if (model.zip == 94086) then BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'County' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'Santa Clara' END 'td' END 'tr' end BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'Zip' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT(model.zip) END 'td' END 'tr' END 'table' END 'div' END 'body' END 'html' ``` ##REST resource handler Add a file `handler-address.lua` to `luaw_root_dir/webapps/myapp/handlers` containing following code: ```lua GET 'address/:city/#zip' { function(req, resp, pathParams) address = { city = pathParams.city, zip = pathParams.zip } return '/views/view-address.lua', address end } ``` This resource handler handles GET request made to URL path "address/_city_/_zip_" with two path parameters - "city" defined as string parameter (denoted by preceding ':') and "zip" defined as numeric (denoted by preceding '#') Most interesting line in the handler above is the following return statement from the function handler: return '/views/view-address.lua', address So far we have been returning a single string from resource handler function (return "Hello World", for example) which Luaw took as a whole response body. This is a second, alternative form. In this form we return two values from the handler function - Lua as a language allows returning multiple values from a function which is very handy - a string and any other value. Whenever this form is used Luaw automatically interpretes first string returned as a relative path to a Luaw template view and the second value to be a "model" that is to be passed to the "view" defined by the Luaw template view. The Luaw template view path is always relative to the application root (`luaw_root_dir/webapps/myapp` in case of our example here) and always starts with a "/". The second value returned - the "model" - can be of any type - number, string, boolean or a Lua table. Our example resource handler above reads values for city and zip code from its REST path parameters and puts them in a single Lua table which it then returns as a model. Our Luaw template view in the step 1 above - view-address.lua - gets access to this model passed from the resource handler using variable `model` ##Modified web.lua Modify **/webapps/myapp/web.lua to include the "viewPattern" element that defines a view pattern so Luaw can load any Luaw template view definitions found under "myapp" directory luaw_webapp = { resourcePattern = "handler%-.*%.lua", viewPattern = "view%-.*%.lua", } ##Test your work Finally, restart your luaw server by running cd luaw_root_dir ./bin/luaw_server ./conf/server.cfg You should see console output similar to this: ``` ********************* Starting webapp myapp **************************** .Loading resource ./webapps/myapp/handlers/handler-address.lua .Loading resource ./webapps/myapp/handlers/handler-hellouser.lua .Loading resource ./webapps/myapp/handlers/handler-helloworld.lua #Loaded total 3 resources .Loading view /views/view-address.lua #Compiled total 1 views *********** ********* Webapp myapp started **************************** ``` Note the "loading view" part. Now point your browser to http://127.0.0.1:7001/myapp/address/Sunnyvale/94085 and see the output in your browser. To verify that the Lua conditional logic embedded in view-address.lua is working properly, point your browser to http://127.0.0.1:7001/myapp/address/Sunnyvale/94086 and see the output. It should include one additional row for the county now. ================================================ FILE: docs/user-threads.md ================================================ #11. User Threads Luaw allows developer to spawn user threads to execute multiple tasks in parallel. Luaw user threads are implemented using Lua's coroutines and hence are very lightweight compared to real OS threads. In addition to this Luaw also pools and reuses Lua coroutines underlying user threads to make spawning user threads even cheaper. As a result you should be able to spawn thousands of them without having to worry about using up all your system resources. Here is a small example of user threads' usage. It uses async HTTP client introduced in last chapter to do two HTTP calls in parallel using user threads, waits till both of them return and then processes both the responses received. It is an example of common scatter/gather pattern frequently encountered in service oriented architecture and illustrates how Luaw's user threads nicely complement Luaw's async HTTP client's functionality ```lua local function parallelHttpRequest(host, url) local clientReq = Luaw.newClientHttpRequest() clientReq.hostName = host clientReq.method = 'GET' clientReq.url = url clientReq:addHeader("Host", host) local clientResp = clientReq:execute() return clientResp end local scheduler = Luaw.scheduler -- do two HTTP request in parallel local threadCtx1 = scheduler.startUserThread(parallelHttpRequest, "www.google.com", "/") local threadCtx2 = scheduler.startUserThread(parallelHttpRequest, "www.facebook.com", "/") -- wait on both threads to be donw scheduler.join(threadCtx1, threadCtx2) -- Retrieve the responses received local clientResp1 = threadCtx1.result local clientResp2 = threadCtx2.result ``` 1. You use Luaw.scheduler.startUserThread(function, ...) to start a new user thread. First argument to this method must be a "thread function" that is to be run by the thread being spawn. This function argument may be followed by variable number of arguments which are passed to the thread function as its argument. In case of the example above our thread function is "parallelHttpRequest" which takes two arguments - a hostname and a URL. These two arguments are passed in Luaw.scheduler.startUserThread() after the thread function in the same order. Luaw.scheduler.startUserThread() re-uses internally pooled coroutine - if one is available - to run the thread function provided so this call is really cheap. 2. Luaw.scheduler.startUserThread() returns a thread context which you can pass to scheduler.join() to wait on the thread to complete. scheduler.join() accepts variable number of thread contexts so you can wait on more than one thread in a single call. scheduler.join() doesn't return till all the threads represented by thread contexts passed into it have finished executing. 3. Value returned by the thread function ("parallelHttpRequest" in our case) can be retrieved as threadCtx.result. Thread function should return only single value (Lua allows functions to return multiple values). If there is a need to return multiple values from the thread function, the function can stuff them all inside a single Lua table with different keys (i.e. property names) and return the Lua table instead. 4. Finally, in all other aspects user threads are semantically similar to system threads spawned by Luaw server itself to server incoming HTTP requests. That is, they can use async calls like HTTP client's execute or Timer methods (explained in the next chapter) and Luaw will automatically suspend them when they are waiting for the async calls to return. They are fully hooked into Luaw's internal async callback mechanism. ================================================ FILE: docs/user-timers.md ================================================ #12. Luaw Timers Luaw supports user defined timers. Here is an example: ```lua local timer = Luaw.newTimer() timer:start(1000) doSomeStuff() print("waiting till it's time...") timer:wait() print('done waiting!') -- call timer: delete() to free timer resources immediately. If not delete() is not called -- timer resources hang around till Lua's VM garbage collects them timer:delete() ``` 1. You create a new timer using Luaw.newTimer() 2. You start it with some timeout - specified in milliseconds using timer:start(timeout) 3. and finally you wait on it using timer:wait() That's basically it! There is one more call - `timer:sleep(timeout)` - that combines `timer:start()` and `timer:wait()` in a single function call. The example above can be rewritten as follows provided we did not have to call doSomeStuff() in between: ```lua local timer = Luaw.newTimer() timer:sleep(1000) print('done waiting!') ``` Luaw timers are fully hooked into Luaw's async machinert. Just like any other async call - HTTP client's execute(), for example - timer:wait() or timer:sleep() suspend current Luaw thread till the time is up. ================================================ FILE: lib/luapack.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local luaw_constants = require("luaw_constants") local lpackMT = getmetatable(luaw_lpack_lib.newLPackParser()) lpackMT.INT_RANGES = { lpackMT.UINT_8, lpackMT.UINT_16, lpackMT.UINT_32, lpackMT.INT_8, lpackMT.INT_16, lpackMT.INT_32, lpackMT.INT_64, lpackMT.FLOAT, lpackMT.DOUBLE } lpackMT.FLOAT_RANGES = { lpackMT.FLOAT, lpackMT.DOUBLE } lpackMT.STRING_RANGES = { lpackMT.STRING, lpackMT.BIG_STRING, lpackMT.HUGE_STRING } lpackMT.DICT_ENTRY_RANGES = { lpackMT.DICT_ENTRY, lpackMT.BIG_DICT_ENTRY } local function findMinRange(num, ranges) for i, range in ipairs(ranges) do if ((num >= range[4])and(num <= range[5])) then return range end end error("Number "..num.." outside supported max range") end -- read functions local function readNextBuffer(lpack) if not lpack.EOF then local newBuffer = lpack:readFn() if not newBuffer then lpack.EOF = true else local buffer = lpack.buffer local offset = lpack.offset if ((buffer)and(#buffer > offset)) then lpack.buffer = string.sub(buffer, offset)..newBuffer else lpack.buffer = newBuffer end lpack.offset = 0 end end end local function done(lpack) return ((lpack.EOF)and(lpack.offset >= #lpack.buffer)) end local function readNumber(lpack, numType) while (not lpack:done()) do local offset = lpack.offset local readLen, value = lpack.read_number(numType, lpack.buffer, offset) if (readLen < 0) then error("Error while reading number at byte# "..tostring(offset).." in buffer: "..tostring(lpack.buffer)) end if (readLen > 0) then lpack.offset = offset + readLen return value end readNextBuffer(lpack); end end local function readMarker(lpack) return readNumber(lpack, lpack.TYPE_MARKER[2]) end local function readString(lpack, desiredLen) local accm while ((desiredLen > 0)and(not lpack:done())) do local offset = lpack.offset local buffer = lpack.buffer local readLen, value = lpack.read_string(desiredLen, buffer, offset) if (readLen > 0) then lpack.offset = offset + readLen desiredLen = desiredLen - readLen if (desiredLen == 0) then if accm then table.insert(accm, value) return table.concat(accm) end return value end if not accm then accm = {} end table.insert(accm, value) end readNextBuffer(lpack); end end local function deserialize(lpack, container, isMap) local key, val, len, t local isKey = true local dictionary = lpack.dictionary while not lpack:done() do t = readMarker(lpack) if t == lpack.NIL[2] then val = nil elseif t == lpack.BOOL_TRUE[2] then val = true elseif t == lpack.BOOL_FALSE[2] then val = false elseif t == lpack.STRING[2] then len = readNumber(lpack, lpack.UINT_8[2]) val = readString(lpack, len) elseif t == lpack.BIG_STRING[2] then len = readNumber(lpack, lpack.UINT_16[2]) val = readString(lpack, len) elseif t == lpack.HUGE_STRING[2] then len = readNumber(lpack, lpack.UINT_32[2]) val = readString(lpack, len) elseif t == lpack.MAP_START[2] then val = deserialize(lpack, luaw_lpack_lib.createDict(0, 16), true) elseif t == lpack.ARRAY_START[2] then val = deserialize(lpack, luaw_lpack_lib.createDict(16, 0), false) elseif t == lpack.RECORD_END[2] then if ((isMap)and(not isKey)) then error("Unbalanced table, key without corresponding value found") end return container elseif t == lpack.DICT_ENTRY[2] then local dw = readNumber(lpack, lpack.UINT_8[2]) assert(dictionary, "Missing dictionary") val = assert(dictionary[dw], "Entry missing in dictionary: "..dw) elseif t == lpack.BIG_DICT_ENTRY[2] then local dw = readNumber(lpack, lpack.UINT_16[2]) assert(dictionary, "Missing dictionary") val = assert(dictionary[dw], "Entry missing in dictionary") elseif t == lpack.DICT_START[2] then dictionary = deserialize(lpack, luaw_lpack_lib.createDict(64, 0), false) lpack.dictionary = dictionary debugDump(dictionary) else -- everything else is a number val = readNumber(lpack, t) end if container then if isMap then if isKey then key = val isKey = false else container[key] = val isKey = true end else -- is array table.insert(container, val) end else if (t ~= lpack.DICT_START[2]) then -- single, standalone value return val end end end return val end local function read(lpack) readNextBuffer(lpack) val = deserialize(lpack, nil, false) return val end local function newLPackReader() local lpackReader = luaw_lpack_lib.newLPackParser(); lpackReader.EOF = false lpackReader.buffer = '' lpackReader.offset = 0 lpackReader.done = done lpackReader.read = read return lpackReader end local function newLPackStringReader(str) assert(str, "String cannot be null") local lpackReader = newLPackReader() local eof = false lpackReader.readFn = function() if (not eof) then eof = true return str end end return lpackReader end local function newLPackFileReader(file) assert(file, "File cannot be null") local lpackReader = newLPackReader() lpackReader.readFn = function() return file:read(1024) end return lpackReader end local function newLPackReqReader(req) assert(req, "Request cannot be null") local lpackReader = newLPackReader() lpackReader.readFn = function() if ((not req.EOF)and(not req.luaw_mesg_done)) then req:readAndParse() local str = req:consumeBodyChunkParsed() if (not str) then debugDump(req) end return str end end return lpackReader end -- Write functions local function flush(lpack) local writeQ = lpack.writeQ local count = #writeQ if count then local str = lpack.serialize_write_Q(writeQ, lpack.writeQsize) if str then lpack:writeFn(str) lpack.writeQsize = 0 for i=1,count do writeQ[i] = nil end end end end local function qstore(lpack, val, size) if not val then error("nil string passed to write(), use writeNil() instead") end local writeQ = lpack.writeQ table.insert(writeQ, val); lpack.writeQsize = lpack.writeQsize + size; if (lpack.writeQsize >= lpack.flushLimit) then flush(lpack) end end local function writeMarker(lpack, marker) if ((marker[2] < lpack.TYPE_MARKER[2])or(marker[2] > lpack.HUGE_STRING[2])) then error("Invalid marker "..marker.." specified") end qstore(lpack, marker[2], 1) end local function startMap(lpack) writeMarker(lpack, lpack.MAP_START) end local function startArray(lpack) writeMarker(lpack, lpack.ARRAY_START) end local function startDict(lpack) writeMarker(lpack, lpack.DICT_START) end local function endCollection(lpack) writeMarker(lpack, lpack.RECORD_END) end local function writeBoolean(lpack, value) if (value) then writeMarker(lpack, lpack.BOOL_TRUE) else writeMarker(lpack, lpack.BOOL_FALSE) end end local function writeNil(lpack) writeMarker(lpack, lpack.NIL) end local function writeNumber(lpack, num) local range if (num % 1 == 0) then -- integer range = findMinRange(num, lpack.INT_RANGES) else -- float range = findMinRange(num, lpack.FLOAT_RANGES) end qstore(lpack, range[2], 1); qstore(lpack, num, range[3]); end local function writeString(lpack, str) local dw, len, range local dict = lpack.dictionary if dict then dw = dict[str] end if dw then range = findMinRange(dw, lpack.DICT_ENTRY_RANGES) str = dw len = range[3] else len = #str range = findMinRange(len, lpack.STRING_RANGES) end qstore(lpack, range[2], 1) -- marker qstore(lpack, str, len) -- actual string value/dictionary entry end local function serialize(lpack, val) local t = type(val) if t == 'nil' then writeNil(lpack) return; end if t == 'boolean' then writeBoolean(lpack, val) return end if t == 'number' then writeNumber(lpack, val) return end if t == 'string' then writeString(lpack, val) return end if t == 'table' then if (#val > 0) then writeMarker(lpack, lpack.ARRAY_START) for i, v in ipairs(val) do serialize(lpack, v) end endCollection(lpack) else writeMarker(lpack, lpack.MAP_START) for k, v in pairs(val) do serialize(lpack, k) serialize(lpack, v) end endCollection(lpack) end end end local function write(lpack, val) serialize(lpack, val) flush(lpack) end local function setDictionaryForWrite(lpack, dict) assert((type(dict) == 'table'), "Please provide valid dictionary table") local dictionary = luaw_lpack_lib.createDict(#dict, 0) writeMarker(lpack, lpack.DICT_START) for i, dw in ipairs(dict) do writeString(lpack, dw) dictionary[dw] = i end writeMarker(lpack, lpack.RECORD_END) lpack.dictionary = dictionary end local function newLPackWriter(limit) local lpackWriter = luaw_lpack_lib.newLPackParser() lpackWriter.writeQ = {} lpackWriter.writeQsize = 0 lpackWriter.flushLimit = limit or luaw_constants.CONN_BUFFER_SIZE lpackWriter.useDictionary = setDictionaryForWrite lpackWriter.write = write return lpackWriter end local function newLPackFileWriter(file, limit) assert(file, "File can not be nil") local lpackWriter = newLPackWriter(limit) lpackWriter.writeFn = function(lpack, str) file:write(str) end return lpackWriter end local function newLPackBufferWriter(buff, limit) assert(buff, "buffer can not be nil") local lpackWriter = newLPackWriter(limit) lpackWriter.writeFn = function(lpack, str) table.insert(buff, str) end return lpackWriter end local function newLPackRespWriter(resp, limit) assert(resp, "response can not be nil") local lpackWriter = newLPackWriter(limit) resp.headers['Content-Type'] = 'application/luapack' resp:startStreaming() lpackWriter.writeFn = function(lpack, str) resp:appendBody(str) end return lpackWriter end luaw_lpack_lib.newLPackFileReader = newLPackFileReader luaw_lpack_lib.newLPackStringReader = newLPackStringReader luaw_lpack_lib.newLPackReqReader = newLPackReqReader luaw_lpack_lib.newLPackFileWriter = newLPackFileWriter luaw_lpack_lib.newLPackBufferWriter = newLPackBufferWriter luaw_lpack_lib.newLPackRespWriter = newLPackRespWriter return luaw_lpack_lib ================================================ FILE: lib/luaw_constants.lua ================================================ local constMT = { __newindex = function(table, key, value) error("constant "..table.name.." cannot be changed") end, __tostring = function(table) return table.name end, __concat = function(op1, op2) return tostring(op1)..tostring(op2) end, __metatable = "Luaw constant" } local function luaw_constant(value) local c = {name = value} setmetatable(c, constMT) return c end return { -- scheduler constants TS_RUNNABLE = luaw_constant("RUNNABLE"), TS_DONE = luaw_constant("DONE"), TS_BLOCKED_EVENT = luaw_constant("BLOCKED_ON_EVENT"), TS_BLOCKED_THREAD = luaw_constant("BLOCKED_ON_THREAD"), END_OF_CALL = luaw_constant("END_OF_CALL"), END_OF_THREAD = luaw_constant("END_OF_THREAD"), -- TCP constants DEFAULT_CONNECT_TIMEOUT = luaw_server_config.connect_timeout or 8000, DEFAULT_READ_TIMEOUT = luaw_server_config.read_timeout or 3000, DEFAULT_WRITE_TIMEOUT = luaw_server_config.write_timeout or 3000, CONN_BUFFER_SIZE = luaw_server_config.connection_buffer_size or 4096, -- HTTP parser constants EOF = 0, CRLF = '\r\n', MULTIPART_BEGIN = luaw_constant("MULTIPART_BEGIN"), PART_BEGIN = luaw_constant("PART_BEGIN"), PART_DATA = luaw_constant("PART_DATA"), PART_END = luaw_constant("PART_END"), MULTIPART_END = luaw_constant("MULTIPART_END") } ================================================ FILE: lib/luaw_data_structs_lib.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local module = {} -- -- Registry --- -- reg[0] stores head of the free list local function ref(reg, obj) if not obj then return -1 end local ref = reg[0]; if ref then reg[0] = reg[ref] else ref = #reg + 1 end reg[ref] = obj reg.size = reg.size + 1 return ref end local function unref(reg, ref) if ref >= 0 then reg[ref] = reg[0] reg[0] = ref reg.size = reg.size -1 end end function module.newRegistry(size) local reg = {} reg.ref = ref; reg.unref = unref; reg.size = 0 return reg; end -- -- Ring buffers -- local function offer(rb, obj) local size = rb.size local filled = rb.filled local writer = rb.writer if (filled < size) then rb[writer] = obj rb.filled = filled + 1 if (writer == size) then rb.writer = 1 else rb.writer = writer + 1 end return true; end return false; end local function offerWithWait(rb, obj) local added = offer(rb, obj) while not added do coroutine.yield() added = offer(rb, obj) end return added end local function take(rb) local size = rb.size local filled = rb.filled local reader = rb.reader local obj = nil if (filled > 0) then obj = rb[reader]; rb[reader] = nil; rb.filled = filled - 1 if (reader == size) then rb.reader = 1 else rb.reader = reader + 1 end end return obj end local function takeWithWait(rb) local obj = take(rb) while not obj do coroutine.yield() obj = take(rb) end return obj end local function offerWithOverwrite(rb, obj) local added = offer(rb, obj) if added then return true end -- overwrite oldest item local overwrittenObj = take(rb) offer(rb, obj) return false, overwrittenObj end function module.newRingBuffer(size) local rb = {} rb.reader = 1 rb.writer = 1 rb.filled = 0 rb.size = size rb.offer = offer rb.take = take return rb end function module.newOverwrittingRingBuffer(size) local rb = {} rb.reader = 1 rb.writer = 1 rb.filled = 0 rb.size = size rb.offer = offerWithOverwrite rb.take = take return rb end function module.newBlockingRingBuffer(size) local rb = {} rb.reader = 1 rb.writer = 1 rb.filled = 0 rb.size = size rb.offer = offerWithWait rb.take = takeWithWait return rb end return module ================================================ FILE: lib/luaw_http.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local constants = require('luaw_constants') local luaw_tcp_lib = require('luaw_tcp') local TS_BLOCKED_EVENT = constants.TS_BLOCKED_EVENT local TS_RUNNABLE = constants.TS_RUNNABLE local CONN_BUFFER_SIZE = constants.CONN_BUFFER_SIZE local EOF = constants.EOF local CRLF = constants.CRLF local MULTIPART_BEGIN = constants.MULTIPART_BEGIN local PART_BEGIN = constants.PART_BEGIN local PART_DATA = constants.PART_DATA local PART_END = constants.PART_END local MULTIPART_END = constants.MULTIPART_END local http_status_codes = { [100] = "Continue", [101] = "Switching Protocols", [200] = "OK", [201] = "Created", [202] = "Accepted", [203] = "Non-Authoritative Information", [204] = "No Content", [205] = "Reset Content", [206] = "Partial Content", [300] = "Multiple Choices", [301] = "Moved Permanently", [302] = "Found", [303] = "See Other", [304] = "Not Modified", [305] = "Use Proxy", [307] = "Temporary Redirect", [400] = "Bad Request", [401] = "Unauthorized", [402] = "Payment Required", [403] = "Forbidden", [404] = "Not Found", [405] = "Method Not Allowed", [406] = "Not Acceptable", [407] = "Proxy Authentication Required", [408] = "Request Timeout", [409] = "Conflict", [410] = "Gone", [411] = "Length Required", [412] = "Precondition Failed", [413] = "Request Entity Too Large", [414] = "Request-URI Too Long", [416] = "Requested Range Not Satisfiable", [417] = "Expectation Failed", [500] = "Internal Server Error", [501] = "Not Implemented", [502] = "Bad Gateway", [503] = "Service Unavailable", [504] = "Gateway Timeout", [505] = "HTTP Version Not Supported" } setmetatable(http_status_codes, { __index = function(status) return "User Defined Status" end }) -- write buffer implementation local function clearArrayPart(t) local len = #t for i=1,len do t[i] = nil end end local function reset(buffer) clearArrayPart(buffer) buffer.len = 0 end local function concat(buffer) return table.concat(buffer) end local function append(buffer, str) local len = buffer.len if (str) then table.insert(buffer, str) len = len + #str buffer.len = len end return len end local function newBuffer() return { len = 0, reset = reset, concat = concat, append = append } end function luaw_http_lib.storeHttpParam(params, name , value) oldValue = params[name] if (oldValue) then -- handle multi-valued param names if (type(oldValue) == 'table') then table.insert(oldValue, value) else -- single param value already stored against the same param name -- convert it to table and store multiple values in it params[name] = {oldValue, value} end else params[name] = value end end local parserMT = getmetatable(luaw_http_lib.newHttpRequestParser()) --[[ HTTP parser we use can invoke callback for the same HTTP field (status, URL, header name/value etc.) multiple times, each time passing only few characters for the current ongoing field. This can happen because we are reading HTTP request or response body in multiple chuncks - either of a fixed byte buffer size of by new lines. For this reason we "accumulate" http header name and value in a hidden request table fields (_acc_header_name_ /_acc_header_value_) and then store full header value against full header name when the parser issues callback for a next HTTP field. Other fields like URL, status etc. are accumulated by concatenating them "in place". ]] local function accumulateChunkedValue(req, name, chunk) local accValue = rawget(req, name) if accValue then rawset(req, name, accValue .. chunk) else rawset(req, name, chunk) end end local function addHeader(req, hName, hValue) if (hName and hValue) then local headers = req.headers local currValues = headers[hName] if currValues then -- handle multi-valued headers if (type(currValues) == 'table') then table.insert(currValues, hValue) else -- single string header value already stored against the same header name -- convert it to table and store multiple values in it headers[hName] = {currValues, hValue} end else headers[hName] = hValue end return true end return false end local function handleAccHttpHeader(req) local hName = rawget(req, '_acc_header_name_') local hValue = rawget(req, '_acc_header_value_') local added = addHeader(req, hName, hValue) if (added) then rawset(req, '_acc_header_name_', nil) rawset(req, '_acc_header_value_', nil) end end local function handleKeepAlive(req, keepAlive) if not keepAlive then req.headers['Connection'] = 'close' req.EOF = true end end local function onNone(req, cbType) end local function onMesgBegin(req, cbtype, remaining) req:reset() end local function onStatus(req, cbtype, remaining ,status) accumulateChunkedValue(req, 'statusMesg', status) end local function onURL(req, cbtype, remaining, url) accumulateChunkedValue(req, 'url', url) end local function onHeaderName(req, cbtype, remaining, hName) handleAccHttpHeader(req) accumulateChunkedValue(req, '_acc_header_name_', hName) end local function onHeaderValue(req, cbtype, remaining, hValue) if not hValue then hValue = '' end -- empty header value accumulateChunkedValue(req, '_acc_header_value_', hValue) end local function onHeadersComplete(req, cbtype, remaining, keepAlive, httpMajor, httpMinor, method, status) handleAccHttpHeader(req) handleKeepAlive(req, keepAlive) req.major_version = httpMajor req.minor_version = httpMinor req.method = method req.status = status -- parse URL local url = req.url local parsedURL if url then local method = req.method parsedURL = luaw_http_lib.parseURL(url, ((method) and (string.upper(method) == "CONNECT"))) else parsedURL = {} end req.parsedURL = parsedURL -- GET query params local params = {} local queryString = parsedURL.queryString if queryString then assert(luaw_http_lib:urlDecode(queryString, params)) end req.params = params req.luaw_headers_done = true end local function onBody(req, cbtype, remaining, chunk) req.bodyParts:append(chunk) end local function onMesgComplete(req, cbtype, remaining, keepAlive) -- for the rare boundary case of chunked transfer encoding, where headers may continue -- after the last body chunk handleAccHttpHeader(req) handleKeepAlive(req, keepAlive) local luaw_parser = req.luaw_parser if (luaw_parser) then luaw_parser:initHttpParser() end -- store body local bodyParts = req.bodyParts req.body = bodyParts:concat() bodyParts:reset() -- POST form params local params = req.params local contentType = req.headers['Content-Type'] if ((contentType) and (contentType:lower() == 'application/x-www-form-urlencoded')) then assert(luaw_http_lib:urlDecode(req.body, params)) end req.luaw_mesg_done = true end -- Order is important and must match C enum http_parser_cb_type local http_callbacks_lua = { onNone, onMesgBegin, onStatus, onURL, onHeaderName, onHeaderValue, onHeadersComplete, onBody, onMesgComplete } local function parseHttpFragment(req, conn, parser, content, offset) -- matched against most number of return results possible. Actual variable names -- are meaningless without the context of correct callback, misleading even! local cbtype, offset, keepAlive, httpMajor, httpMinor, method, status = parser:parseHttp(content, offset) if (not cbtype) then conn:close() return error(offset) -- offset carries error message in this case end local callback = http_callbacks_lua[cbtype] if (not callback) then conn:close() return error("Invalid HTTP parser callback# "..tostring(cbtype).." requested") end callback(req, cbtype, remaining, keepAlive, httpMajor, httpMinor, method, status) return cbtype, offset end local function hasContent(content, offset) return (content)and(offset)and(offset < #content) end local function readAndParse(req) local conn = req.luaw_conn local parser = req.luaw_parser local content = req.luaw_read_content local offset = req.luaw_read_offset local httpcb, status if (not hasContent(content, offset)) then -- read new content from socket status, content = conn:read(req.readTimeout) if (not status) then if (content == 'EOF') then req:addHeader('Connection', 'close') req.luaw_read_content = nil req.luaw_read_offset = nil req.luaw_headers_done = true req.luaw_mesg_done = true req.EOF = true return else return error(content) end end offset = 0 -- offset is for C, therefore zero based end httpcb, offset = parseHttpFragment(req, conn, parser, content, offset) if (hasContent(content,offset)) then -- store back remaining content in request object for next HTTP request parsing req.luaw_read_content = content req.luaw_read_offset = offset else req.luaw_read_content = nil req.luaw_read_offset = nil end end local function consumeTill(input, search, offset) local start, stop = string.find(input, search, offset, true) if (start and stop) then return stop+1, string.sub(input, offset, start-1) end end local function getMultipartBoundary(req) local header = req.headers['Content-Type'] if (header) then local offset, contentType = consumeTill(header, ";", 1) if (contentType == "multipart/form-data") then local boundary offset, boundary = consumeTill(header, "=", offset) if ((boundary)and(string.find(boundary, "boundary", 1, true))) then return '--'..string.sub(header, offset) end end end end local function isMultipart(req) if (req.luaw_multipart_boundary) then return true end local boundary = getMultipartBoundary(req) if (boundary) then req.luaw_multipart_boundary = boundary req.luaw_multipart_end = boundary .. '--' return true end end local function isLuaPackMesg(req) local contentType = req.headers['Content-Type'] if ('application/luapack' == contentType) then return true end end local function readFull(req) -- first parse till headers are done while (not req.luaw_headers_done) do req:readAndParse() end if ((isMultipart(req))or(isLuaPackMesg(req))) then -- multipart (file upload) HTTP requests and LuaPack requests are forced to be streaming to conserve memory return end while (not req.luaw_mesg_done) do req:readAndParse() end end local function consumeBodyChunkParsed(req) local bodyChunk = req.body if (bodyChunk) then req.body = nil else local bodyParts = req.bodyParts if (bodyParts.len > 0) then bodyChunk = bodyParts:concat() bodyParts:reset() end end return bodyChunk end local function bufferedConsume(req, search, content, offset) assert(search, "search pattern cannot be nil") while (true) do if ((content)and(#content > offset)) then local matchPos, matchStr = consumeTill(content, search, offset) if (matchPos) then -- found match if (matchPos < #content) then -- there is content remaining return matchStr, content, matchPos end -- content fully consumed return matchStr, nil, nil end if (offset > 1) then content = string.sub(content, offset) offset = 1 end end -- match not found, read more req:readAndParse() if (req.luaw_mesg_done) then error("premature HTTP message end") end local readStr = req:consumeBodyChunkParsed() if (readStr) then if (content) then content = content..readStr else content = readStr offset = 1 end end end end local function saveState(req, content, offset) req.luaw_multipart_content = content req.luaw_multipart_offset = offset end function fetchNextPart(req, state) local content = req.luaw_multipart_content local offset = req.luaw_multipart_offset or 1 local boundary = req.luaw_multipart_boundary local matchStr if (state == MULTIPART_BEGIN) then -- read beginning boundary matchedStr, content, offset = bufferedConsume(req, CRLF, content, offset) assert(matchedStr == boundary, "Missing multi-part boundary at the beginning of the part") state = PART_END end if (state == PART_END) then -- read "Content-Disposition" line matchedStr, content, offset = bufferedConsume(req, ": ", content, offset) assert(matchedStr == 'Content-Disposition',"Missing 'Content-Disposition' header") matchedStr, content, offset = bufferedConsume(req, "; ", content, offset) assert(string.find(matchedStr, 'form-data', 1, true), "Wrong Content-Disposition") matchedStr, content, offset = bufferedConsume(req, '"', content, offset) assert(string.find(matchedStr, "name", 1, true), "form field name missing") local fieldName fieldName, content, offset = bufferedConsume(req, '"', content, offset) assert(#fieldName > 0, "form field name missing") matchedStr, content, offset = bufferedConsume(req, CRLF, content, offset) local _, filenamePos = string.find(matchedStr, 'filename="', 1, true) local fileName if (filenamePos) then fileName = string.sub(matchedStr, filenamePos+1, #matchedStr-1) end local contentType if (fileName) then -- read "Content-Type" line matchedStr, content, offset = bufferedConsume(req, ": ", content, offset) assert(matchedStr == 'Content-Type', "Missing 'Content-Type' header") contentType, content, offset = bufferedConsume(req, CRLF, content, offset) assert(#contentType, "Content-Type value missing") end -- read next blank line matchedStr, content, offset = bufferedConsume(req, CRLF, content, offset) assert(#matchedStr == 0, "Missing line separating content headers and actual content") saveState(req, content, offset) return PART_BEGIN, fieldName, fileName, contentType end while ((state == PART_BEGIN)or(state == PART_DATA)) do local matchedStr, content, offset = bufferedConsume(req, CRLF, content, offset) if (matchedStr) then if (matchedStr == boundary) then saveState(req, content, offset) return PART_END end local lastBoundary = req.luaw_multipart_end if (matchedStr == lastBoundary) then saveState(req, content, offset) return MULTIPART_END end saveState(req, content, offset) return PART_DATA, matchedStr else local content = req.luaw_multipart_content local offset = req.luaw_consumed_till or 0 if ((content)and(#content-offset) > #boundary+3) then saveState(req, nil, 1) return PART_DATA, content end end end end local function multiPartIterator(req) if (isMultipart(req)) then return fetchNextPart, req, MULTIPART_BEGIN end end local function shouldCloseConnection(req) if req and req.EOF then return true end end local function toURLSafeChar(ch) if (ch == " ") then return "+" end return string.format("%%%02X", string.byte(ch)) end local function urlEncode(str) str = string.gsub(str, "([^a-zA-Z0-9%.%*%-_])", toURLSafeChar) return str end local function urlEncodeParams(params) if params then local encodedParams = {} local contentLength = 0 for key, val in pairs(params) do local ueKey = urlEncode(key) table.insert(encodedParams, ueKey) contentLength = contentLength + #ueKey table.insert(encodedParams, "=") contentLength = contentLength + 1 local ueVal = urlEncode(val) table.insert(encodedParams, ueVal) contentLength = contentLength + #ueVal table.insert(encodedParams, "&") contentLength = contentLength + 1 end if (#encodedParams > 0) then table.remove(encodedParams) -- remove the last extra "&" contentLength = contentLength - 1 return encodedParams, contentLength end end return nil, 0 end function buildURL(req) if (req.method == 'GET') then local encodedParams = urlEncodeParams(req.params) if encodedParams then table.insert(encodedParams, 1, "?") table.insert(encodedParams, 1, req.url) return table.concat(encodedParams) end end return req.url end local function setStatus(resp, status) resp.status = status resp.statusMesg = http_status_codes[status] end local function getStatus(resp) return resp.status end local function getBody(resp) return resp.body end local function firstResponseLine(resp) local line = {"HTTP/", resp.major_version, ".", resp.minor_version, " ", resp.status, " ", resp.statusMesg, CRLF} return table.concat(line) end local function firstRequestLine(req) local line = {req.method, " ", req:buildURL(), " HTTP/", req.major_version, ".", req.minor_version, CRLF} return table.concat(line) end local function sendBuffer(buffer, conn, writeTimeout, isChunked) local chunk = table.concat(buffer) buffer:reset() if (isChunked) then chunk = string.format("%x\r\n", #chunk)..chunk..CRLF end conn:write(chunk, writeTimeout) end local function bufferHeader(buffer, name, value) buffer:append(tostring(name)) buffer:append(": ") buffer:append(tostring(value)) buffer:append(CRLF) end local function bufferHeaders(headers, buffer) if (headers) then for name,value in pairs(headers) do if (type(value) == 'table') then for i,v in ipairs(value) do bufferHeader(buffer, name, v) end else bufferHeader(buffer, name, value) end headers[name] = nil end end buffer:append(CRLF) end local function startStreaming(resp) resp.luaw_is_chunked = true resp:addHeader('Transfer-Encoding', 'chunked') local headers = resp.headers local conn = resp.luaw_conn local writeTimeout = resp.writeTimeout -- use separate buffer from "bodyParts" to serialize headers local headersBuffer = newBuffer() headersBuffer:append(resp:firstLine()) bufferHeaders(headers, headersBuffer) -- flush up to HTTP headers end without chunked encoding before actual body starts sendBuffer(headersBuffer, conn, writeTimeout, false) end local function appendBody(resp, bodyPart) if not bodyPart then return end local bodyBuffer = resp.bodyParts local len = bodyBuffer:append(bodyPart) if ((resp.luaw_is_chunked)and(len >= CONN_BUFFER_SIZE)) then local conn = resp.luaw_conn local writeTimeout = resp.writeTimeout sendBuffer(bodyBuffer, conn, writeTimeout, true) end end local function writeFullBody(resp) local conn = resp.luaw_conn local writeTimeout = resp.writeTimeout local bodyBuffer = resp.bodyParts if (resp.method == 'POST') then local encodedParams, contentLength = urlEncodeParams(resp.params) if encodedParams then resp:addHeader('Content-Type', 'application/x-www-form-urlencoded') bodyBuffer:append(encodedParams) end end resp:addHeader('Content-Length', bodyBuffer.len) -- first write up to HTTP headers end local headersBuffer = newBuffer() headersBuffer:append(resp:firstLine()) bufferHeaders(resp.headers, headersBuffer) sendBuffer(headersBuffer, conn, writeTimeout, false) -- now write body sendBuffer(bodyBuffer, conn, writeTimeout, false) end local function endStreaming(resp) local conn = resp.luaw_conn local writeTimeout = resp.writeTimeout local bodyBuffer = resp.bodyParts -- flush whatever is remaining in write buffer sendBuffer(bodyBuffer, conn, writeTimeout, true) -- add last chunk encoding trailer conn:write("0\r\n\r\n", writeTimeout) end local function flush(resp) if resp.luaw_is_chunked then endStreaming(resp) else writeFullBody(resp) end end local function close(req) local conn = req.luaw_conn; if conn then conn:close() req.luaw_conn = nil end end local function reset(req) req.headers = {} req["_acc_header_name_"] = nil req["_acc_header_value_"] = nil req.url = nil req.bodyParts:reset() req.body = nil req.luaw_mesg_done = nil req.luaw_headers_done = nil req.params = nil req.parsedURL = nil req.status = nil req.statusMesg = nil end luaw_http_lib.newServerHttpRequest = function(conn) local req = { luaw_mesg_type = 'sreq', luaw_conn = conn, headers = {}, bodyParts = newBuffer(), luaw_parser = luaw_http_lib:newHttpRequestParser(), addHeader = addHeader, shouldCloseConnection = shouldCloseConnection, isComplete = isComplete, readAndParse = readAndParse, readFull = readFull, isMultipart = isMultipart, multiPartIterator = multiPartIterator, getBody = getBody, reset = reset, consumeBodyChunkParsed = consumeBodyChunkParsed, close = close } return req; end luaw_http_lib.newServerHttpResponse = function(conn) local resp = { luaw_mesg_type = 'sresp', luaw_conn = conn, major_version = 1, minor_version = 1, contentLength = 0, headers = {}, bodyParts = newBuffer(), addHeader = addHeader, shouldCloseConnection = shouldCloseConnection, setStatus = setStatus, firstLine = firstResponseLine, startStreaming = startStreaming, appendBody = appendBody, flush = flush, reset = reset, close = close } return resp; end local function newClientHttpResponse(conn) local resp = { luaw_mesg_type = 'cresp', luaw_conn = conn, headers = {}, bodyParts = newBuffer(), luaw_parser = luaw_http_lib:newHttpResponseParser(), addHeader = addHeader, shouldCloseConnection = shouldCloseConnection, readAndParse = readAndParse, getBody = getBody, getStatus = getStatus, readFull = readFull, consumeBodyChunkParsed = consumeBodyChunkParsed, reset = reset, close = close } return resp; end local function connect(req) local conn = assert(luaw_tcp_lib.connect(req.hostIP, req.hostName, req.port, req.connectTimeout)) return conn end local function connectReq(req) conn = connect(req) conn:startReading() req.luaw_conn = conn local resp = newClientHttpResponse(conn) resp.readTimeout = req.readTimeout resp.writeTimeout = req.writeTimeout return resp end local function execute(req) local resp = req:connect() req:flush() resp:readFull() if resp:shouldCloseConnection() then resp:close() end return resp end luaw_http_lib.newClientHttpRequest = function() local req = { luaw_mesg_type = 'creq', port = 80, major_version = 1, minor_version = 1, method = 'GET', contentLength = 0, headers = {}, bodyParts = newBuffer(), addHeader = addHeader, connect = connectReq, execute = execute, shouldCloseConnection = shouldCloseConnection, buildURL = buildURL, firstLine = firstRequestLine, startStreaming = startStreaming, appendBody = appendBody, flush = flush, reset = reset, close = close } return req; end return luaw_http_lib ================================================ FILE: lib/luaw_init.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] luaw_utils = require("luaw_utils") luaw_logging = require("luaw_logging") luaw_lpack = require("luapack") luaw_scheduler = require("luaw_scheduler") luaw_tcp = require("luaw_tcp") luaw_timer = require("luaw_timer") luaw_http = require("luaw_http") luaw_webapp = require("luaw_webapp") luaw_webapp.init() ================================================ FILE: lib/luaw_logging.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local ds_lib = require('luaw_data_structs_lib') local luaw_utils_lib = require("luaw_utils") local luapack_lib = require('luapack') local log_module = {} local PATH_SEPARATOR = string.match (package.config, "[^\n]+") -- Log file states local LOG_NOT_OPEN = 0 local OPENING_LOG = 1 local LOG_IS_OPEN = 2 -- Log levels local EMERGENCY = 0 local ALERT = 1 local CRITICAL = 2 local ERROR = 3 local WARNING = 4 local NOTICE = 5 local INFO = 6 local DEBUG = 7 log_module.EMERGENCY = EMERGENCY log_module.ALERT = ALERT log_module.CRITICAL = CRITICAL log_module.ERROR = ERROR log_module.WARNING = WARNING log_module.NOTICE = NOTICE log_module.INFO = INFO log_module.DEBUG = DEBUG -- Log appender types local FILE_LOG = "FILE" local SYS_LOG = "SYSLOG" log_module.SYSLOG_FACILITY_USER = 1 log_module.SYSLOG_FACILITY_AUTH = 10 log_module.SYSLOG_FACILITY_AUDIT = 13 log_module.SYSLOG_FACILITY_ALERT = 14 log_module.SYSLOG_FACILITY_LOCAL0 = 16 log_module.SYSLOG_FACILITY_LOCAL1 = 17 log_module.SYSLOG_FACILITY_LOCAL2 = 18 log_module.SYSLOG_FACILITY_LOCAL3 = 19 log_module.SYSLOG_FACILITY_LOCAL4 = 20 log_module.SYSLOG_FACILITY_LOCAL5 = 21 log_module.SYSLOG_FACILITY_LOCAL6 = 22 log_module.SYSLOG_FACILITY_LOCAL7 = 23 local logRoot = { } local logDir = assert(luaw_log_config.log_dir, "Invalid log directory specified") local noOfLogLinesToBuffer = luaw_log_config.log_lines_buffer_count or 100 local logfileBaseName = luaw_log_config.log_file_basename or "luaw-log" local logfileSizeLimit = luaw_log_config.log_file_size_limit or (1024 * 1024 * 10) -- 10MB local logfileCountLimit = luaw_log_config.log_file_count_limit or 99 local logLineTimeFormat = luaw_log_config.log_line_timestamp_format or "%x %X" local logFileNameTimeFormat = luaw_log_config.log_filename_timestamp_format or '%Y%m%d-%H%M%S' local syslogTag = luaw_log_config.syslog_tag or 'luaw' local syslogPresent = luaw_logging_lib.syslogConnect(luaw_log_config.syslog_server, luaw_log_config.syslog_port) logRoot.facility = luaw_log_config.syslog_facility or log_module.SYSLOG_FACILITY_LOCAL7 local hostname = luaw_logging_lib.hostname() local logSequenceNum = 0 local logSize = 0 local logBuffer = ds_lib.newOverwrittingRingBuffer(noOfLogLinesToBuffer + 32) local noOfLogLinesDropped = 0 local currentTimeStr local syslogTimeStr log_module.updateCurrentTime = function(currentTime) currentTimeStr = os.date(logLineTimeFormat, currentTime) if syslogPresent then syslogTimeStr = os.date("%b %d %X", currentTime) end end local function nextLogSequenceNum() if logSequenceNum > logfileCountLimit then logSequenceNum = 0 end logSequenceNum = logSequenceNum + 1 return logSequenceNum end local function concatLogLines() local temp = luapack_lib.createDict(logBuffer.filled+1, 0) local i = 1 local logLine = logBuffer:take() while logLine do temp[i] = logLine i = i+1 logLine = logBuffer:take() end temp[i] = '' -- for the last newline return table.concat(temp, '\n') end local function logToFile(logLine) local added = logBuffer:offer(currentTimeStr..' '..logLine) if not added then noOfLogLinesDropped = noOfLogLinesDropped +1 end local state = luaw_logging_lib.logState() if ((state == LOG_IS_OPEN)and(logBuffer.filled >= noOfLogLinesToBuffer)) then local logBatch = concatLogLines() logSize = logSize + string.len(logBatch) local rotateLog = (logSize >= logfileSizeLimit) state = luaw_logging_lib.writeLog(logBatch, rotateLog) end if (state == LOG_NOT_OPEN) then logSize = 0 local ts = os.date(logFileNameTimeFormat, os.time()) local fileName = logDir..PATH_SEPARATOR..logfileBaseName..'-'..ts..'-'..nextLogSequenceNum()..'.log' luaw_logging_lib.openLog(fileName) end end local function syslog(priority, facility, mesg) local pri = priority + (facility * 8) local logLine = string.format("<%d>%s %s %s: %s", pri, syslogTimeStr, hostname, syslogTag, mesg) luaw_logging_lib.syslogSend(logLine); end local nameIterator = luaw_utils_lib.splitter('.') local function splitName(name) if not name then return luaw_utils_lib.nilFn end return nameIterator, name, 0 end local function logInternal(logLevel, fileLevel, syslogLevel, syslogFacility, mesg) if (logLevel <= fileLevel) then logToFile(mesg) end if ((syslogPresent)and(logLevel <= syslogLevel)) then syslog(logLevel, syslogFacility, mesg) end end local function log(logger, logLevel, mesg) local fileLevel = logger[FILE_LOG] or ERROR local syslogLevel = logger[SYS_LOG] or ERROR logInternal(logLevel, fileLevel, syslogLevel, logger.facility, mesg) end local function logf(logger, logLevel, mesgFormat, ...) local fileLevel = logger[FILE_LOG] or ERROR local syslogLevel = logger[SYS_LOG] or ERROR if ((logLevel <= fileLevel)or(logLevel <= syslogLevel)) then local mesg = string.format(mesgFormat, ...) logInternal(logLevel, fileLevel, syslogLevel, logger.facility, mesg) end end logRoot.log = log logRoot.logf = logf logRoot.emergency = function(logger, mesg) log(logger, EMERGENCY, mesg) end logRoot.alert = function(logger, mesg) log(logger, ALERT, mesg) end logRoot.critical = function(logger, mesg) log(logger, CRITICAL, mesg) end logRoot.error = function(logger, mesg) log(logger, ERROR, mesg) end logRoot.warning = function(logger, mesg) log(logger, WARNING, mesg) end logRoot.notice = function(logger, mesg) log(logger, NOTICE, mesg) end logRoot.info = function(logger, mesg) log(logger, INFO, mesg) end logRoot.debug = function(logger, mesg) log(logger, DEBUG, mesg) end logRoot.emergencyf = function(logger, mesgFormat, ...) logf(logger, EMERGENCY, mesgFormat, ...) end logRoot.alertf = function(logger, mesgFormat, ...) logf(logger, ALERT, mesgFormat, ...) end logRoot.criticalf = function(logger, mesgFormat, ...) logf(logger, CRITICAL, mesgFormat, ...) end logRoot.errorf = function(logger, mesgFormat, ...) logf(logger, ERROR, mesgFormat, ...) end logRoot.warningf = function(logger, mesgFormat, ...) logf(logger, WARNING, mesgFormat, ...) end logRoot.noticef = function(logger, mesgFormat, ...) logf(logger, NOTICE, mesgFormat, ...) end logRoot.infof = function(logger, mesgFormat, ...) logf(logger, INFO, mesgFormat, ...) end logRoot.debugf = function(logger, mesgFormat, ...) logf(logger, DEBUG, mesgFormat, ...) end local function getLogger(name) local logger = logRoot if (name == 'root') then return logger end for idx, namePart in splitName(name) do local child = logger[namePart] if not child then child = {} setmetatable(child, {__index = logger}) logger[namePart] = child end logger = child end return logger end log_module.getLogger = getLogger local function configureLogger(logCfg, logType) local loggerName = assert(logCfg.name, "Logger name missing") local logLevel = assert(logCfg.level, "Logger level missing") local logger = assert(getLogger(loggerName), "Could not find logger "..loggerName) logger[logType] = logLevel return logger end log_module.file = function(logCfg) configureLogger(logCfg, FILE_LOG) end log_module.syslog = function(logCfg) local logger = configureLogger(logCfg, SYS_LOG) logger.facility = logCfg.facility end return log_module ================================================ FILE: lib/luaw_scheduler.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local constants = require('luaw_constants') local ds_lib = require('luaw_data_structs_lib') local logging = require('luaw_logging') -- Scheduler object local scheduler = {} -- Constants local TS_RUNNABLE = constants.RUNNABLE local TS_DONE = constants.DONE local TS_BLOCKED_EVENT = constants.BLOCKED_ON_EVENT local TS_BLOCKED_THREAD = constants.BLOCKED_ON_THREAD local END_OF_CALL = constants.END_OF_CALL local END_OF_THREAD = constants.END_OF_THREAD local UPDATE_TIME_COUNTER_LIMIT = 10 -- scheduler state local threadRegistry = ds_lib.newRegistry(luaw_server_config.thread_pool_size or 1024) local threadPool = ds_lib.newRingBuffer(luaw_server_config.thread_pool_size or 1024) local timesReuseThread = luaw_server_config.thread_reuse_limit or 1024 local runQueueLen = 0 local runQueueHead = nil local runQueueTail = nil local currentRunningThreadCtx = nil local updateTimeCyclingCounter = 0 local currentTime scheduler.updateCurrentTime = function () updateTimeCyclingCounter = updateTimeCyclingCounter + 1 if (updateTimeCyclingCounter >= UPDATE_TIME_COUNTER_LIMIT) then currentTime = os.time() logging.updateCurrentTime(currentTime) updateTimeCyclingCounter = 0 end end scheduler.time = function() return currentTime end -- returns current running thread's id scheduler.tid = function() if currentRunningThreadCtx then return currentRunningThreadCtx.tid end return nil end local function threadRunLoop(fn, arg1, arg2, arg3, arg4) local i = 0 while i <= timesReuseThread do fn, arg1, arg2, arg3, arg4 = coroutine.yield(END_OF_CALL, fn(arg1, arg2, arg3, arg4)) i = i+1 end return END_OF_THREAD, fn(arg1, arg2, arg3, arg4) end local function userThreadRunner(userThreadFn, ...) -- We have captured user thread function along with its arguments on a coroutine stack. -- Yield now so that scheduler can add this thread in run queue for "bottom half" -- processing later and original calling thread can resume. coroutine.yield(TS_RUNNABLE) -- At this point we have been resumed by thread scheduler during the "bottom half" run -- queue processing bu the scheduler so run the actual user thread function. return userThreadFn(...) end local function addToRunQueue(threadCtx) if not runQueueTail then runQueueHead = threadCtx runQueueTail = threadCtx else runQueueTail.nextThread = threadCtx runQueueTail = threadCtx end runQueueLen = runQueueLen + 1 threadCtx.state = TS_RUNNABLE end local function newThread() local t = threadPool:take() if not t then t = coroutine.create(threadRunLoop) end local threadCtx = { thread = t, requestCtx = {} } -- anchor thread in registry to prevent GC local ref = threadRegistry:ref(threadCtx) threadCtx.tid = ref return threadCtx end local function unblockJoinedThreadIfAny(threadCtx, status, retVal) local joinedTC = threadCtx.joinedBy if joinedTC then local count = joinedTC.joinCount count = count -1 joinedTC.joinCount = count if (count <= 0) then addToRunQueue(joinedTC) end end end local function afterResume(threadCtx, state, retVal) threadCtx.state, threadCtx.result = state, retVal currentRunningThreadCtx = nil if (state == TS_DONE) then return true, retVal end return false, retVal end local function resumeThread(threadCtx, ...) currentRunningThreadCtx = threadCtx local t = threadCtx.thread local tid = threadCtx.tid scheduler.updateCurrentTime() context = threadCtx.requestCtx -- TLS, per thread context local status, state, retVal = coroutine.resume(t, ...) context = nil -- reset TLS context if not status then -- thread ran into error print("Error: "..tostring(state)) state = END_OF_THREAD -- thread has blown its stack so let it get garbage collected t = nil end if ((state == END_OF_THREAD)or(state == END_OF_CALL)) then threadRegistry:unref(tid) threadCtx.thread = nil threadCtx.requestCtx = nil if ((state == END_OF_CALL) and (t)) then -- thread is still alive, return it to free pool if possible threadPool:offer(t) end unblockJoinedThreadIfAny(threadCtx, status, retVal) return afterResume(threadCtx, TS_DONE, retVal) end if ((state == TS_BLOCKED_EVENT)or(state == TS_BLOCKED_THREAD)) then -- thread will later be resumed by libuv call back return afterResume(threadCtx, state, retVal) end -- Thread yielded, but is still runnable. Add it back to the run queue addToRunQueue(threadCtx) return afterResume(threadCtx, TS_RUNNABLE, retVal) end function resumeThreadId(tid, ...) local threadCtx = threadRegistry[tid] if not threadCtx then error("Invalid thread Id "..tostring(tid)) end return resumeThread(threadCtx, ...) end scheduler.resumeThreadId = resumeThreadId function startSystemThread(serviceFn, conn, ...) local threadCtx = newThread() threadCtx.state = TS_RUNNABLE local isDone = resumeThread(threadCtx, serviceFn, conn, ...) return isDone, threadCtx.tid end scheduler.startSystemThread = startSystemThread -- Scheduler object methods scheduler.startUserThread = function(userThreadFn, ...) local backgroundThreadCtx = newThread() coroutine.resume(backgroundThreadCtx.thread, userThreadRunner, userThreadFn, ...) addToRunQueue(backgroundThreadCtx) return backgroundThreadCtx; end scheduler.join = function(...) local joiningTC = currentRunningThreadCtx if (joininTC) then local joinedThreads = table.pack(...) local numOfThreads = #joinedThreads local count = 0 for i, joinedTC in ipairs(joinedThreads) do if ((joinedTC)and(joinedTC.state)and(joinedTC.state ~= TS_DONE)) then count = count + 1 joinedTC.joinedBy = joiningTC end end joiningTC.joinCount = count while (joiningTC.joinCount > 0) do coroutine.yield(TS_BLOCKED_THREAD) end end end scheduler.runQueueSize = function() return runQueueLen end local runNextFromRunQueue = function() local threadCtx = runQueueHead if threadCtx then runQueueHead = threadCtx.nextThread if not runQueueHead then runQueueTail = nil end threadCtx.nextThread = nil runQueueLen = runQueueLen -1 if (runQueueLen < 0) then runQueueLen = 0 end if (threadCtx.state == TS_DONE) then -- This can happen when thread is added to the run queue but is woken up by libuv -- event and then runs to completion before the run queue scheduler gets chance -- to resume it return end return resumeThread(threadCtx) end end scheduler.runReadyThreads = function(limit) local runnableCount = runQueueLen if ((limit)and(limit < runnableCount)) then runnableCount = limit end for i=1, runnableCount do runNextFromRunQueue() end -- about to block on libuv event loop, next resumeThread should update current time -- as it may have spent significant time blocked on a event loop. updateTimeCyclingCounter = UPDATE_TIME_COUNTER_LIMIT return runnableCount end scheduler.updateCurrentTime() return scheduler ================================================ FILE: lib/luaw_tcp.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local constants = require('luaw_constants') local scheduler = require('luaw_scheduler') local DEFAULT_CONNECT_TIMEOUT = constants.DEFAULT_CONNECT_TIMEOUT local DEFAULT_READ_TIMEOUT = constants.DEFAULT_READ_TIMEOUT local DEFAULT_WRITE_TIMEOUT = constants.DEFAULT_WRITE_TIMEOUT local CONN_BUFFER_SIZE = constants.CONN_BUFFER_SIZE local conn = luaw_tcp_lib.newConnection(); local connMT = getmetatable(conn) conn:close() local startReadingInternal = connMT.startReading local readInternal = connMT.read local writeInternal = connMT.write connMT.startReading = function(self) local status, mesg = startReadingInternal(self) assert(status, mesg) end connMT.read = function(self, readTimeout) local status, str = readInternal(self, scheduler.tid(), readTimeout or DEFAULT_READ_TIMEOUT) if ((status)and(not str)) then -- nothing in buffer, wait for libuv on_read callback status, str = coroutine.yield(TS_BLOCKED_EVENT) end return status, str end connMT.write = function(self, str, writeTimeout) local status, nwritten = writeInternal(self, scheduler.tid(), str, writeTimeout or DEFAULT_WRITE_TIMEOUT) if ((status)and(nwritten > 0)) then -- there is something to write, yield for libuv callback status, nwritten = coroutine.yield(TS_BLOCKED_EVENT) end assert(status, nwritten) return nwritten end local connectInternal = luaw_tcp_lib.connect local function connect(hostIP, hostName, port, connectTimeout) assert((hostName or hostIP), "Either hostName or hostIP must be specified in request") local threadId = scheduler.tid() if not hostIP then local status, mesg = luaw_tcp_lib.resolveDNS(hostName, threadId) assert(status, mesg) status, mesg = coroutine.yield(TS_BLOCKED_EVENT) assert(status, mesg) hostIP = mesg end local connectTimeout = connectTimeout or DEFAULT_CONNECT_TIMEOUT local conn, mesg = connectInternal(hostIP, port, threadId, connectTimeout) -- initial connect_req succeeded, block for libuv callback assert(coroutine.yield(TS_BLOCKED_EVENT)) return conn, mesg end luaw_tcp_lib.connect = connect return luaw_tcp_lib ================================================ FILE: lib/luaw_timer.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local constants = require('luaw_constants') local TS_BLOCKED_EVENT = constants.TS_BLOCKED_EVENT local timerMT = getmetatable(luaw_timer_lib.newTimer()) local waitInternal = timerMT.wait timerMT.wait = function(timer) local status, elapsed = waitInternal(timer, scheduler.tid()) if ((status) and (not elapsed)) then -- timer not yet elapsed, wait for libuv on_timeout callback status, elapsed = coroutine.yield(TS_BLOCKED_EVENT) end return status, elapsed end timerMT.sleep = function(timer, timeout) assert(timer:start(timeout)) timer:wait() end return luaw_timer_lib ================================================ FILE: lib/luaw_utils.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local luaw_util_lib = {} local function tprint(tbl, indent, tab) for k, v in pairs(tbl) do if type(v) == "table" then print(string.rep(tab, indent) .. tostring(k) .. ": {") tprint(v, indent+1, tab) print(string.rep(tab, indent) .. "}") else print(string.rep(tab, indent) .. tostring(k) .. ": " .. tostring(v)) end end end -- Print contents of `tbl`, with indentation. -- `indent` sets the initial level of indentation. luaw_util_lib.debugDump = function(tbl, indent, tab) indent = indent or 0 tab = tab or " " print(string.rep(tab, indent) .. "{") tprint(tbl, indent+1, tab) print(string.rep(tab, indent) .. "}") end luaw_util_lib.steplight = function(mesg) local tid = tostring(Luaw.scheduler.tid()) print("Thread-"..tid.."> "..tostring(mesg)) end luaw_util_lib.step = function(mesg, level) local tid = tostring(Luaw.scheduler.tid()) local lvl = level or 2 if (lvl < 0) then lvl = lvl * -1 end local dc = debug.getinfo(lvl, "nSl") local str = "" if type(mesg) == 'table' then for k,v in pairs(mesg) do str = str..", "..tostring(k).."="..tostring(v) end else str = tostring(mesg) end print('Thread '..tid..'> line# '..tostring(dc.linedefined)..' in function '..tostring(dc.name)..' in file '..tostring(dc.source)..': '..str) if ((level)and(level < 0)) then print(debug.traceback()) end end luaw_util_lib.run = function(codeblock) if (codeblock) then local try = codeblock.try if (try) then local catch = codeblock.catch local finally = codeblock.finally local status, err = pcall(try, codeblock) if ((not status)and(catch)) then status, err = pcall(catch, codeblock, err) end if (finally) then finally(codeblock) end if (not status) then error(err) end end end end luaw_util_lib.clearArrayPart = function(t) local len = #t for i=1,len do t[i] = nil end end luaw_util_lib.splitter = function(splitCh) local separator = string.byte(splitCh, 1, 1) local byte = string.byte return function (str, pos) pos = pos + 1 local start = pos local len = #str while pos <= len do local ch = byte(str, pos, pos) if (ch == separator) then if (pos > start) then return pos, string.sub(str, start, pos-1) end start = pos + 1 end pos = pos + 1 end if (pos > start) then return pos, string.sub(str, start, pos) end end end luaw_util_lib.nilFn = function() return nil end luaw_util_lib.formattedLine = function(str, lineSize, paddingCh, beginCh, endCh) lineSize = lineSize or 0 paddingCh = paddingCh or '' beginCh = beginCh or '' endCh = endCh or '' paddingWidth = (lineSize - #str -2)/2 local padding = '' if paddingWidth > 0 then padding = string.rep(paddingCh, paddingWidth) end print(string.format("%s %s %s %s %s", beginCh, padding, str, padding, endCh)) end return luaw_util_lib ================================================ FILE: lib/luaw_webapp.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local luaw_utils_lib = require("luaw_utils") local luaw_http_lib = require("luaw_http") local HTTP_METHODS = { GET = "GET", POST = "POST", PUT = "PUT", DELETE = "DELETE", HEAD = "HEAD", OPTIONS = "OPTIONS", TRACE = "TRACE", CONNECT = "CONNECT", SERVICE = "SERVICE" } local registeredWebApps = {} local DIR_SEPARATOR = string.match (package.config, "[^\n]+") local STRING_PATH_PARAM = { start = string.byte(":"), valueOf = tostring } local NUM_PATH_PARAM = { start = string.byte("#"), valueOf = tonumber } TAB = ' ' local function findFiles(path, pattern, matches) if (path and pattern) then for file in lfs.dir(path) do if (file ~= '.' and file ~= '..') then local f = path..DIR_SEPARATOR..file local attrs = lfs.attributes(f) if attrs then local mode = attrs.mode if mode == 'file' then if (string.match(f, pattern)) then table.insert(matches, f) end elseif mode == 'directory' then findFiles(f, pattern, matches) end end end end end return matches end local pathIterator = luaw_utils_lib.splitter('/') local function splitPath(path) if not path then return luaw_util_lib.nilFn end return pathIterator, path, 0 end local function findAction(method, path) assert(method, "HTTP method may not be nil") assert(method, "HTTP request path may not be nil") local webApp = nil local route = nil local pathParams = {} for idx, pseg in splitPath(path) do -- first iteration only if not webApp then webApp = registeredWebApps[pseg] if not webApp then -- fallback to root path webApp = registeredWebApps['/'] end if not webApp then return nil end route = webApp.root else local nextRoute = route.childRoutes[pseg] if not nextRoute then -- may be it's a path param nextRoute = route.childRoutes["_path_param_"] if nextRoute then pathParams[nextRoute.pathParam] = nextRoute.pathParamType.valueOf(pseg) end end if not nextRoute then return nil end route = nextRoute end end if (route) then local action = route[method] if not action then -- try catch call action as a fallback action = route['SERVICE'] end return webApp, action, pathParams end end local function renderView(req, resp, pathParams, model, view) local isTagOpen = false local indent = 0 local attributes = function(attrs) if attrs then for k,v in pairs(attrs) do resp:appendBody(' ') resp:appendBody(k) resp:appendBody('="') resp:appendBody(tostring(v)) resp:appendBody('"') end end end local BEGIN = function(tag) if (isTagOpen) then resp:appendBody('>\n') end for i=1, indent do resp:appendBody(TAB) end resp:appendBody('<') resp:appendBody(tag) isTagOpen = true; indent = indent+1 return attributes end local END = function(tag) if (isTagOpen) then resp:appendBody('>\n') isTagOpen = false; end indent = indent - 1 if indent > 0 then indent = indent else indent = 0 end for i=1, indent do resp:appendBody(TAB) end resp:appendBody('\n') end local TEXT = function(...) if (isTagOpen) then resp:appendBody('>\n') isTagOpen = false; end for i=1, indent do resp:appendBody(TAB) end local values = {...} for i,v in ipairs(values) do resp:appendBody(tostring(v)) resp:appendBody(' ') end resp:appendBody('\n') end -- render view if ((req.major_version >= 1)and(req.minor_version >= 1)) then resp:startStreaming() end view(req, resp, pathParams, model, BEGIN, TEXT, END) end local function dispatchAction(req, resp) assert(req, "HTTP request may not be nil") local parsedURL = req.parsedURL if not(req.method and parsedURL) then -- EOF in case of persistent connections return end local webApp, action, pathParams = findAction(req.method, parsedURL.path) assert(action, "No action found for path "..parsedURL.path.." for method "..req.method) if req:shouldCloseConnection() then resp.headers['Connection'] = 'close' else resp.headers['Connection'] = 'Keep-Alive' end v1, v2 = action.handler(req, resp, pathParams) -- handle action returned response, if any and if resp is not closed if v1 and resp.luaw_conn then if (type(v1) == 'number') then -- v1 is HTTP status resp:setStatus(v1) --resp:startStreaming() if v2 then -- v2 is body content resp:appendBody(tostring(v2)) end else if not resp.statusCode then resp:setStatus(200) end --resp:startStreaming() if ((type(v1) == 'string')and(v2)) then -- v1 is view path, v2 is view model local compiledView = webApp.compiledViews[v1] if not compiledView then error("View '"..tostring(v1).."' is not defined") end renderView(req, resp, pathParams, v2, compiledView) else -- v1 is the body content itself resp:appendBody(tostring(v1)) end end end -- flush in case resp is not closed if resp.luaw_conn then resp:flush() end end local function registerResource(resource) local route = assert(webapp.root, "webapp root not defined") local path = assert(resource.path , "Handler definition is missing value for 'path'") local handlerFn = assert(resource.handler, "Handler definition is missing 'handler' function") local method = resource.method or 'SERVICE' if(not HTTP_METHODS[method]) then error(method.." is not a valid HTTP method") end for idx, pseg in splitPath(path) do local firstChar = string.byte(pseg) local pathParam = nil if ((firstChar == STRING_PATH_PARAM.start)or(firstChar == NUM_PATH_PARAM.start)) then pathParam = pseg:sub(2) pseg = "_path_param_" end local nextRoute = route.childRoutes[pseg] if not nextRoute then nextRoute = { childRoutes = {} } if pathParam then nextRoute.pathParam = pathParam if (firstChar == NUM_PATH_PARAM.start) then nextRoute.pathParamType = NUM_PATH_PARAM else nextRoute.pathParamType = STRING_PATH_PARAM end end route.childRoutes[pseg] = nextRoute end route = nextRoute end assert(route, "Could not register handler for path "..path) assert((not route[method]), 'Handler already registered for '..method..' for path "/'..webapp.path..'/'..path..'"') route[method] = {handler = handlerFn} end local function serviceHTTP(conn) conn:startReading() -- loop to support HTTP 1.1 persistent (keep-alive) connections while true do local req = luaw_http_lib.newServerHttpRequest(conn) -- read and parse full request local status, errmesg = pcall(req.readFull, req) if ((not status)or(req.EOF == true)) then conn:close() if (status) then return "read time out" end print("Error: ", errmesg, debug.traceback()) return "connection reset by peer" end local resp = luaw_http_lib.newServerHttpResponse(conn) local status, errMesg = pcall(dispatchAction, req, resp) if (not status) then -- send HTTP error response resp:setStatus(500) resp:addHeader('Connection', 'close') pcall(resp.appendBody, resp, errMesg) pcall(resp.flush, resp) conn:close() error(errMesg) end if (req:shouldCloseConnection() or resp:shouldCloseConnection()) then conn:close() return "connection reset by peer" end end end local function toFullPath(appRoot, files) local fullPaths = {} if files then for i, file in ipairs(files) do table.insert(fullPaths, appRoot..'/'..file) end end return fullPaths end local function loadWebApp(appName, appDir) local app = {} app.path = assert(appName, "Missing mandatory configuration property 'path'") app.appRoot = assert(appDir, "Missing mandatory configuration property 'root_dir'") if (registeredWebApps[app.path]) then error('Anothe web app is already registered for path '..app.path) end registeredWebApps[app.path] = app app.root = { childRoutes = {} } -- Load resource handlers local resources = toFullPath(app.appRoot, luaw_webapp.resources) if luaw_webapp.resourcePattern then resources = findFiles(app.appRoot, luaw_webapp.resourcePattern, resources) end assert((resources and (#resources > 0)), "Either 'resources' or 'resourcePattern' must be specified in a web app configuration") app.resources = resources -- Load view files if any local views = toFullPath(app.appRoot, luaw_webapp.views) if luaw_webapp.viewPattern then views = findFiles(app.appRoot, luaw_webapp.viewPattern, views) end app.views = views return app end local function loadView(viewPath, buff) local firstLine = true local lastLine = false local viewLines = io.lines(viewPath) return function() local line = nil if firstLine then firstLine = false line = "return function(req, resp, pathParams, model, BEGIN, TEXT, END)" else if (not lastLine) then local vl = viewLines() if (not vl) then lastLine = true line = "end" else line = vl end end end if line then table.insert(buff, line) return (line .. '\n') end end end local function startWebApp(app) -- register resources local resources = app.resources for i,resource in ipairs(resources) do luaw_utils_lib.formattedLine(".Loading resource "..resource) -- declare globals (registerHandler and webapp) for the duration of the loadfile(resource) registerHandler = registerResource webapp = app local routeDefn = assert(loadfile(resource), string.format("Could not load resource %s", resource)) routeDefn() end luaw_utils_lib.formattedLine("#Loaded total "..#resources.." resources\n") -- compile views local views = app.views local compiledViews = {} local appRootLen = string.len(app.appRoot) + 1 for i,view in ipairs(views) do local relativeViewPath = string.sub(view, appRootLen) luaw_utils_lib.formattedLine(".Loading view "..relativeViewPath) local viewBuff = {} local viewDefn = loadView(view, viewBuff) local compiledView, errMesg = load(viewDefn, relativeViewPath) if (not compiledView) then luaw_utils_lib.formattedLine("\nError while compiling view: "..view) luaw_utils_lib.formattedLine("") for i, line in ipairs(viewBuff) do print(tostring(i)..":\t"..tostring(line)) end luaw_utils_lib.formattedLine("") error(errMesg) end compiledViews[relativeViewPath] = compiledView() end app.views = nil app.compiledViews = compiledViews luaw_utils_lib.formattedLine("#Compiled total "..#views.." views") end local function init() if ((luaw_webapp_config)and(luaw_webapp_config.base_dir)) then local root = luaw_webapp_config.base_dir for webappName in lfs.dir(root) do if (webappName ~= '.' and webappName ~= '..') then local webappDir = root..DIR_SEPARATOR..webappName local attrs = lfs.attributes(webappDir) if ((attrs)and(attrs.mode == 'directory')) then local webappCfgFile = webappDir..DIR_SEPARATOR..'web.lua' if (lfs.attributes(webappCfgFile, 'mode') == 'file') then luaw_utils_lib.formattedLine('Starting webapp '..webappName, 120, '*', '\n') dofile(webappCfgFile) -- defines global variable luaw_webapp local app = loadWebApp(webappName, webappDir) startWebApp(app) luaw_utils_lib.formattedLine('Webapp '..webappName..' started', 120, '*') webapp = nil -- reset global variable end end end end end end -- install REST HTTP app handler as a default request handler luaw_http_lib.request_handler = serviceHTTP return { init = init, dispatchAction = dispatchAction, serviceHTTP = serviceHTTP } ================================================ FILE: lib/unit_testing.lua ================================================ --[[ Copyright (c) 2015 raksoras Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local module = {total_run = 0, total_failed = 0} function module.assertTrue(expr) if not expr then error("Assert true failed!", 2) end end function module.assertFalse(expr) if expr then error("Assert false failed!", 2) end end function module.assertNotNil(expr) if not expr then error("Assert not nil failed!", 2) end end function module.assertNil(expr) if expr then error("Assert nil failed!", 2) end end function module.assertEqual(actual, expected) if (actual ~= expected) then error(string.format("Assert equal failed! Actual: [%s], Expected: [%s]", actual, expected), 2) end end function module.assertNotEqual(actual, expected) if (actual == expected) then error(string.format("Assert not equal failed! Actual: [%s], Expected: [%s]", actual, expected), 2) end end local function tprint(tbl, indent, tab) for k, v in pairs(tbl) do if type(v) == "table" then print(string.rep(tab, indent) .. tostring(k) .. ": {") tprint(v, indent+1, tab) print(string.rep(tab, indent) .. "}") else print(string.rep(tab, indent) .. tostring(k) .. ": " .. tostring(v)) end end end -- Print contents of `tbl`, with indentation. -- `indent` sets the initial level of indentation. function module.printTable (tbl, indent, tab) indent = indent or 0 tab = tab or " " print(string.rep(tab, indent) .. "{") tprint(tbl, indent+1, tab) print(string.rep(tab, indent) .. "}") end function module:runTests() number_run = 0 number_failed = 0 for name, func in pairs(self) do if (string.find(name, "test") == 1) then result, mesg = pcall(func) number_run = number_run + 1 if (string.find(name, "testError") == 1) then -- negative test if not result then print(string.format(" %-40s [OK]", name)); else number_failed = number_failed + 1 print(string.format(" %-30s [FAILED! Expected to throw error]", name)); end else if result then print(string.format(" %-40s [OK]", name)); else number_failed = number_failed + 1 print(string.format(" %-30s [FAILED! %s]", name, mesg)); end end self[name] = nil end end self.total_run = self.total_run + number_run self.total_failed = self.total_failed + number_failed local info = debug.getinfo(2, "S") print('----------------------------------------------------------------------------') print(string.format("%s: Total# %d, Failed# %d", info.source, number_run, number_failed)); print('----------------------------------------------------------------------------') end function printOverallSummary() print('\n*****************************************************************************') print(string.format("Overall Summary: Total# %d, Failed# %d", module.total_run, module.total_failed)); print('*****************************************************************************\n') end return module; ================================================ FILE: sample/conf/server.cfg ================================================ luaw_server_config = { server_ip = "0.0.0.0", server_port = 7001, connect_timeout = 4000, read_timeout = 8000, write_timeout = 8000 } luaw_log_config = { log_dir = "./logs", log_file_basename = "luaw-log", log_file_size_limit = 1024*1024, log_file_count_limit = 9, log_filename_timestamp_format = '%Y%m%d', log_lines_buffer_count = 16, syslog_server = "127.0.0.1", syslog_port = 514, } luaw_webapp_config = { base_dir = "./webapps" } ================================================ FILE: sample/proxy_handler.lua ================================================ local http_lib = require('luaw_http') --[[ Luaw allows you to replace it's default MVC/REST request handler with your own custom HTTP request handler implementation. To override the default HTTP request handler just set Luaw object's request_handler property to your custom Lua function. This function is passed in a low level connection object for each incoming request instead of the normal request and response objects passed to REST handler. The function is called on its own separate Luaw coroutine for each HTTP request so you don't have to worry about multithreaded access to shared state inside the function. ]] http_lib.request_handler = function(conn) conn:startReading() -- loop to support HTTP 1.1 persistent (keep-alive) connections while true do local req = http_lib.newServerHttpRequest(conn) local resp = http_lib.newServerHttpResponse(conn) -- read and parse full request req:readFull() if (req.EOF) then conn:close() return "connection reset by peer" end local reqHeaders = req.headers local beHost = reqHeaders['backend-host'] local beURL = reqHeaders['backend-url'] if (beHost and beURL) then local backendReq = http_lib.newClientHttpRequest() backendReq.hostName = beHost backendReq.url = beURL backendReq.method = 'GET' backendReq.headers = { Host = beHost } local status, backendResp = pcall(backendReq.execute, backendReq) if (status) then resp:setStatus(backendResp:getStatus()) resp:appendBody(backendResp:getBody()) local beHeaders = backendResp.headers for k,v in pairs(beHeaders) do if ((k ~= 'Transfer-Encoding')and(k ~= 'Content-Length')) then resp:addHeader(k,v) end end backendResp:close() else resp:setStatus(500) resp:appendBody("connection to backend server failed") end else resp:setStatus(400) resp:appendBody("Request must contain headers backend-host and backend-url\n") end local status, mesg = pcall(resp.flush, resp) if (not status) then conn:close() return error(mesg) end if (req:shouldCloseConnection() or resp:shouldCloseConnection()) then conn:close() return "connection reset by peer" end end end ================================================ FILE: sample/webapps/myapp/handlers/handler-address.lua ================================================ registerHandler { method = 'GET', path = 'address/:city/#zip', handler = function(req, resp, pathParams) address = { city = pathParams.city, zip = pathParams.zip } return '/views/view-address.lua', address end } ================================================ FILE: sample/webapps/myapp/handlers/handler-fileupload-display.lua ================================================ registerHandler { method = 'GET', path = 'showform', handler = function(req, resp, pathParams) return [[ upload

]] end } ================================================ FILE: sample/webapps/myapp/handlers/handler-fileupload-process.lua ================================================ local function append(buffer, str) if (str) then table.insert(buffer, str) table.insert(buffer, ", ") end end registerHandler { method = 'POST', path = 'filesupload', handler = function(req, resp, pathParams) if (req:isMultipart()) then local token, fieldName, fileName, contentType local buffer = {} for token, fieldName, fileName, contentType in req:multiPartIterator() do append(buffer, tostring(token)) append(buffer, fieldName) append(buffer, fileName) append(buffer, contentType) table.insert(buffer,"\n") end return table.concat(buffer) end return "Not a multi-part file upload" end } ================================================ FILE: sample/webapps/myapp/handlers/handler-hellouser.lua ================================================ registerHandler { method = 'GET', path = '/user/:username/#count', handler = function(req, resp, pathParams) return "Hello "..pathParams.username.."! You are user number "..pathParams.count.." to visit this site." end } ================================================ FILE: sample/webapps/myapp/handlers/handler-helloworld.lua ================================================ registerHandler { method = 'GET', path = 'helloworld', handler = function(req, resp, pathParams) return "Hello World!" end } ================================================ FILE: sample/webapps/myapp/handlers/handler-read-lpack.lua ================================================ local utils_lib = require('luaw_utils') local http_lib = require('luaw_http') local lpack = require('luapack') registerHandler { method = 'GET', path = 'readlpack', handler = function(req, resp, pathParams) local backendReq = http_lib.newClientHttpRequest() backendReq.hostName = 'localhost' backendReq.port = 7001 backendReq.url = '/myapp/genlpack' backendReq.method = 'GET' backendReq.headers = { Host = 'localhost' } local backendResp = backendReq:execute() local lpackReader = lpack.newLPackReqReader(backendResp) local mesg = lpackReader:read() print('\n================================\n') utils_lib.debugDump(backendResp.headers) print('\n================================\n') utils_lib.debugDump(mesg) print('\n================================\n') return "OK" end } ================================================ FILE: sample/webapps/myapp/handlers/handler-write-lpack.lua ================================================ local utils_lib = require('luaw_utils') local http_lib = require('luaw_http') local lpack = require('luapack') local big_str = "abcdefghijklmnopqrstuvwxyz0123456789" while (#big_str < 4096) do big_str = big_str ..'-'..big_str end local mesg = { name = "Homer Simpson", gender = "M", uint8 = 255, uint16 = 256, uint32 = 4294967295, int8_neg = -128, int8 = 127, int16_neg = -1000, int16 = 20000, int32 = 32456, int64= 17179869184, int64_neg= -17179869184, float = 0.0012, float_neg = - 112.8, double = 8589934592.13, double_neg = -8589934592.28, str = "ABCD", bigstr = big_str, positive = true, negative = false, kids = { { name = "Lisa", gender = "F", uint8 = 255, uint16 = 256, uint32 = 4294967295, int8_neg = -128, int8 = 127, int16_neg = -1000, int16 = 20000, int32 = 32456, int64= 17179869184, int64_neg= -17179869184, float = 0.0012, float_neg = - 112.8, double = 8589934592.13, double_neg = -8589934592.28, str = "ABCD", bigstr = big_str, positive = true, negative = false }, { name = "Bart", gender = "M", uint8 = 255, uint16 = 256, uint32 = 4294967295, int8_neg = -128, int8 = 127, int16_neg = -1000, int16 = 20000, int32 = 32456, int64= 17179869184, int64_neg= -17179869184, float = 0.0012, float_neg = - 112.8, double = 8589934592.13, double_neg = -8589934592.28, str = "ABCD", bigstr = big_str, positive = true, negative = false }, { name = "Maggy", gender = "?" } } } local dict = { "name", "gender", "uint8", "uint16", "uint32", "int8_neg", "int8", "int16_neg", "int16", "int32", "int64", "int64_neg", "float", "float_neg", "double", "double_neg", "str", "bigstr", "positive", "negative", big_str } registerHandler { method = 'GET', path = 'genlpack', handler = function(req, resp, pathParams) resp:setStatus(200) local lpackWriter = lpack.newLPackRespWriter(resp) if (req.params['dict'] == 'true') then lpackWriter:useDictionary(dict) end lpackWriter:write(mesg) end } ================================================ FILE: sample/webapps/myapp/views/view-address.lua ================================================ BEGIN 'html' BEGIN 'head' BEGIN 'title' TEXT 'Address' END 'title' END 'head' BEGIN 'body' BEGIN 'div' {class='address'} BEGIN 'h1' TEXT(model.title) END 'h1' BEGIN 'table' {border="1", margin="1px"} BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'City' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT(model.city) END 'td' END 'tr' if (model.zip == 94086) then BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'County' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'Santa Clara' END 'td' END 'tr' end BEGIN 'tr' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT 'Zip' END 'td' BEGIN 'td' {style="padding: 3px 3px 3px 3px"} TEXT(model.zip) END 'td' END 'tr' END 'table' END 'div' END 'body' END 'html' ================================================ FILE: sample/webapps/myapp/web.lua ================================================ luaw_webapp = { resourcePattern = "handler%-.*%.lua", viewPattern = "view%-.*%.lua", } ================================================ FILE: src/Makefile ================================================ # Makefile for building Luaw # == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT ======================= CC?= gcc CFLAGS?= -O2 -g -Wall CFLAGS+= $(SYSCFLAGS) $(MYCFLAGS) -I../$(UVDIR)/include -I../$(LUADIR)/src LDFLAGS= $(SYSLDFLAGS) $(MYLDFLAGS) LIBS= ../$(UVLIB) -lpthread ../$(LUALIB) -lm $(SYSLIBS) $(MYLIBS) # == END OF USER SETTINGS -- NO NEED TO CHANGE ANYTHING BELOW THIS LINE ======= # Build artifacts LUAW_OBJS= http_parser.o lua_lpack.o luaw_common.o luaw_logging.o luaw_http_parser.o luaw_server.o luaw_tcp.o luaw_timer.o lfs.o LUAW_BIN= luaw_server LUAW_CONF= server.cfg LUAW_SCRIPTS= luapack.lua luaw_init.lua luaw_logging.lua luaw_data_structs_lib.lua luaw_utils.lua \ luaw_scheduler.lua luaw_webapp.lua luaw_timer.lua luaw_tcp.lua luaw_http.lua luaw_constants.lua # How to install. If your install program does not support "-p", then # you may have to run ranlib on the installed liblua.a. INSTALL= install INSTALL_EXEC= $(INSTALL) -p -m 0755 INSTALL_DATA= $(INSTALL) -p -m 0644 # # If you don't have "install" you can use "cp" instead. # INSTALL= cp -p # INSTALL_EXEC= $(INSTALL) # INSTALL_DATA= $(INSTALL) # Other utilities. MKDIR= mkdir -p RM= rm -f RMDIR= rm -rf #where to install INSTALL_BIN=$(INSTALL_ROOT)/bin INSTALL_LIB=$(INSTALL_ROOT)/lib INSTALL_CONF=$(INSTALL_ROOT)/conf INSTALL_LOGS=$(INSTALL_ROOT)/logs INSTALL_WEBAPP=$(INSTALL_ROOT)/webapps # Targets start here. all: $(LUAW_BIN) $(LUAW_BIN): $(LUAW_OBJS) $(CC) -o $@ $(LDFLAGS) $(LUAW_OBJS) $(LIBS) install: check_install_root $(MKDIR) $(INSTALL_BIN) $(MKDIR) $(INSTALL_LIB) $(MKDIR) $(INSTALL_CONF) $(MKDIR) $(INSTALL_LOGS) $(MKDIR) $(INSTALL_WEBAPP) $(INSTALL_EXEC) $(LUAW_BIN) $(INSTALL_BIN) cd ../lib && $(INSTALL_DATA) $(LUAW_SCRIPTS) $(INSTALL_BIN) cd ../conf && $(INSTALL_DATA) $(LUAW_CONF) $(INSTALL_CONF) install-sample: install cd ../sample && cp -r * $(INSTALL_ROOT) uninstall: check_install_root $(RMDIR) $(INSTALL_ROOT)/* check_install_root: ifndef INSTALL_ROOT $(error INSTALL_ROOT is undefined) endif clean: $(RM) $(LUAW_BIN) $(LUAW_OBJS) echo: @echo "CC= $(CC)" @echo "CFLAGS= $(CFLAGS)" @echo "LDFLAGS= $(SYSLDFLAGS)" @echo "LIBS= $(LIBS)" @echo "RM= $(RM)" # list targets that do not create files (but not all makes understand .PHONY) .PHONY: default install install-sample uninstall check_install_root clean echo # Luaw object files http_parser.o: http_parser.c http_parser.h lua_lpack.o: lua_lpack.c lua_lpack.h luaw_common.h luaw_logging.o: luaw_logging.c luaw_logging.h luaw_common.h luaw_common.o: luaw_common.c luaw_common.h luaw_tcp.h luaw_http_parser.h luaw_timer.h lua_lpack.h luaw_http_parser.o: luaw_http_parser.c luaw_http_parser.h luaw_common.h luaw_tcp.h lfs.h luaw_server.o: luaw_server.c luaw_common.h luaw_tcp.h luaw_logging.h luaw_tcp.o: luaw_tcp.c luaw_tcp.h luaw_common.h http_parser.h luaw_http_parser.h luaw_tcp.h luaw_timer.o: luaw_timer.c luaw_timer.h luaw_common.h lfs.o: lfs.c lfs.h ================================================ FILE: src/http_parser.c ================================================ /* Based on src/http/ngx_http_parse.c from NGINX copyright Igor Sysoev * * Additional changes are licensed under the same terms as NGINX and * copyright Joyent, Inc. and other Node contributors. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #include "http_parser.h" #include #include #include #include #include #include #ifndef ULLONG_MAX # define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */ #endif #ifndef MIN # define MIN(a,b) ((a) < (b) ? (a) : (b)) #endif #ifndef ARRAY_SIZE # define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) #endif #ifndef BIT_AT # define BIT_AT(a, i) \ (!!((unsigned int) (a)[(unsigned int) (i) >> 3] & \ (1 << ((unsigned int) (i) & 7)))) #endif #ifndef ELEM_AT # define ELEM_AT(a, i, v) ((unsigned int) (i) < ARRAY_SIZE(a) ? (a)[(i)] : (v)) #endif #define SET_ERRNO(e) \ do { \ parser->http_errno = (e); \ } while(0) /* Run the notify callback FOR, returning ER if it fails */ #define CALLBACK_NOTIFY_(FOR, ER) \ do { \ assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ \ if (settings->on_##FOR) { \ if (0 != settings->on_##FOR(parser)) { \ SET_ERRNO(HPE_CB_##FOR); \ } \ \ /* We either errored above or got paused; get out */ \ if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { \ return (ER); \ } \ } \ } while (0) /* Run the notify callback FOR and consume the current byte */ #define CALLBACK_NOTIFY(FOR) CALLBACK_NOTIFY_(FOR, p - data + 1) /* Run the notify callback FOR and don't consume the current byte */ #define CALLBACK_NOTIFY_NOADVANCE(FOR) CALLBACK_NOTIFY_(FOR, p - data) /* Run data callback FOR with LEN bytes, returning ER if it fails */ #define CALLBACK_DATA_(FOR, LEN, ER) \ do { \ assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ \ if (FOR##_mark) { \ if (settings->on_##FOR) { \ if (0 != settings->on_##FOR(parser, FOR##_mark, (LEN))) { \ SET_ERRNO(HPE_CB_##FOR); \ } \ \ /* We either errored above or got paused; get out */ \ if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { \ return (ER); \ } \ } \ FOR##_mark = NULL; \ } \ } while (0) /* Run the data callback FOR and consume the current byte */ #define CALLBACK_DATA(FOR) \ CALLBACK_DATA_(FOR, p - FOR##_mark, p - data + 1) /* Run the data callback FOR and don't consume the current byte */ #define CALLBACK_DATA_NOADVANCE(FOR) \ CALLBACK_DATA_(FOR, p - FOR##_mark, p - data) /* Set the mark FOR; non-destructive if mark is already set */ #define MARK(FOR) \ do { \ if (!FOR##_mark) { \ FOR##_mark = p; \ } \ } while (0) #define PROXY_CONNECTION "proxy-connection" #define CONNECTION "connection" #define CONTENT_LENGTH "content-length" #define TRANSFER_ENCODING "transfer-encoding" #define UPGRADE "upgrade" #define CHUNKED "chunked" #define KEEP_ALIVE "keep-alive" #define CLOSE "close" static const char *method_strings[] = { #define XX(num, name, string) #string, HTTP_METHOD_MAP(XX) #undef XX }; /* Tokens as defined by rfc 2616. Also lowercases them. * token = 1* * separators = "(" | ")" | "<" | ">" | "@" * | "," | ";" | ":" | "\" | <"> * | "/" | "[" | "]" | "?" | "=" * | "{" | "}" | SP | HT */ static const char tokens[256] = { /* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ 0, 0, 0, 0, 0, 0, 0, 0, /* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ 0, 0, 0, 0, 0, 0, 0, 0, /* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ 0, 0, 0, 0, 0, 0, 0, 0, /* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ 0, 0, 0, 0, 0, 0, 0, 0, /* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ 0, '!', 0, '#', '$', '%', '&', '\'', /* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ 0, 0, '*', '+', 0, '-', '.', 0, /* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ '0', '1', '2', '3', '4', '5', '6', '7', /* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ '8', '9', 0, 0, 0, 0, 0, 0, /* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', /* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', /* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', /* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ 'x', 'y', 'z', 0, 0, 0, '^', '_', /* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', /* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', /* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', /* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ 'x', 'y', 'z', 0, '|', 0, '~', 0 }; static const int8_t unhex[256] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 , 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1 ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 }; #if HTTP_PARSER_STRICT # define T(v) 0 #else # define T(v) v #endif static const uint8_t normal_url_char[32] = { /* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0, /* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128, /* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, /* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, }; #undef T enum state { s_dead = 1 /* important that this is > 0 */ , s_start_req_or_res , s_res_or_resp_H , s_start_res , s_res_H , s_res_HT , s_res_HTT , s_res_HTTP , s_res_first_http_major , s_res_http_major , s_res_first_http_minor , s_res_http_minor , s_res_first_status_code , s_res_status_code , s_res_status_start , s_res_status , s_res_line_almost_done , s_start_req , s_req_method , s_req_spaces_before_url , s_req_schema , s_req_schema_slash , s_req_schema_slash_slash , s_req_server_start , s_req_server , s_req_server_with_at , s_req_path , s_req_query_string_start , s_req_query_string , s_req_fragment_start , s_req_fragment , s_req_http_start , s_req_http_H , s_req_http_HT , s_req_http_HTT , s_req_http_HTTP , s_req_first_http_major , s_req_http_major , s_req_first_http_minor , s_req_http_minor , s_req_line_almost_done , s_header_field_start , s_header_field , s_header_value_start , s_header_value , s_header_value_lws , s_header_almost_done , s_chunk_size_start , s_chunk_size , s_chunk_parameters , s_chunk_size_almost_done , s_headers_almost_done , s_headers_done /* Important: 's_headers_done' must be the last 'header' state. All * states beyond this must be 'body' states. It is used for overflow * checking. See the PARSING_HEADER() macro. */ , s_chunk_data , s_chunk_data_almost_done , s_chunk_data_done , s_body_identity , s_body_identity_eof , s_message_done }; #define PARSING_HEADER(state) (state <= s_headers_done) enum header_states { h_general = 0 , h_C , h_CO , h_CON , h_matching_connection , h_matching_proxy_connection , h_matching_content_length , h_matching_transfer_encoding , h_matching_upgrade , h_connection , h_content_length , h_transfer_encoding , h_upgrade , h_matching_transfer_encoding_chunked , h_matching_connection_keep_alive , h_matching_connection_close , h_transfer_encoding_chunked , h_connection_keep_alive , h_connection_close }; enum http_host_state { s_http_host_dead = 1 , s_http_userinfo_start , s_http_userinfo , s_http_host_start , s_http_host_v6_start , s_http_host , s_http_host_v6 , s_http_host_v6_end , s_http_host_port_start , s_http_host_port }; /* Macros for character classes; depends on strict-mode */ #define CR '\r' #define LF '\n' #define LOWER(c) (unsigned char)(c | 0x20) #define IS_ALPHA(c) (LOWER(c) >= 'a' && LOWER(c) <= 'z') #define IS_NUM(c) ((c) >= '0' && (c) <= '9') #define IS_ALPHANUM(c) (IS_ALPHA(c) || IS_NUM(c)) #define IS_HEX(c) (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f')) #define IS_MARK(c) ((c) == '-' || (c) == '_' || (c) == '.' || \ (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \ (c) == ')') #define IS_USERINFO_CHAR(c) (IS_ALPHANUM(c) || IS_MARK(c) || (c) == '%' || \ (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ (c) == '$' || (c) == ',') #if HTTP_PARSER_STRICT #define TOKEN(c) (tokens[(unsigned char)c]) #define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) #define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') #else #define TOKEN(c) ((c == ' ') ? ' ' : tokens[(unsigned char)c]) #define IS_URL_CHAR(c) \ (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) #define IS_HOST_CHAR(c) \ (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') #endif #define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res) #if HTTP_PARSER_STRICT # define STRICT_CHECK(cond) \ do { \ if (cond) { \ SET_ERRNO(HPE_STRICT); \ goto error; \ } \ } while (0) # define NEW_MESSAGE() (http_should_keep_alive(parser) ? start_state : s_dead) #else # define STRICT_CHECK(cond) # define NEW_MESSAGE() start_state #endif /* Map errno values to strings for human-readable output */ #define HTTP_STRERROR_GEN(n, s) { "HPE_" #n, s }, static struct { const char *name; const char *description; } http_strerror_tab[] = { HTTP_ERRNO_MAP(HTTP_STRERROR_GEN) }; #undef HTTP_STRERROR_GEN int http_message_needs_eof(const http_parser *parser); /* Our URL parser. * * This is designed to be shared by http_parser_execute() for URL validation, * hence it has a state transition + byte-for-byte interface. In addition, it * is meant to be embedded in http_parser_parse_url(), which does the dirty * work of turning state transitions URL components for its API. * * This function should only be invoked with non-space characters. It is * assumed that the caller cares about (and can detect) the transition between * URL and non-URL states by looking for these. */ static enum state parse_url_char(enum state s, const char ch) { if (ch == ' ' || ch == '\r' || ch == '\n') { return s_dead; } #if HTTP_PARSER_STRICT if (ch == '\t' || ch == '\f') { return s_dead; } #endif switch (s) { case s_req_spaces_before_url: /* Proxied requests are followed by scheme of an absolute URI (alpha). * All methods except CONNECT are followed by '/' or '*'. */ if (ch == '/' || ch == '*') { return s_req_path; } if (IS_ALPHA(ch)) { return s_req_schema; } break; case s_req_schema: if (IS_ALPHA(ch)) { return s; } if (ch == ':') { return s_req_schema_slash; } break; case s_req_schema_slash: if (ch == '/') { return s_req_schema_slash_slash; } break; case s_req_schema_slash_slash: if (ch == '/') { return s_req_server_start; } break; case s_req_server_with_at: if (ch == '@') { return s_dead; } /* FALLTHROUGH */ case s_req_server_start: case s_req_server: if (ch == '/') { return s_req_path; } if (ch == '?') { return s_req_query_string_start; } if (ch == '@') { return s_req_server_with_at; } if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') { return s_req_server; } break; case s_req_path: if (IS_URL_CHAR(ch)) { return s; } switch (ch) { case '?': return s_req_query_string_start; case '#': return s_req_fragment_start; } break; case s_req_query_string_start: case s_req_query_string: if (IS_URL_CHAR(ch)) { return s_req_query_string; } switch (ch) { case '?': /* allow extra '?' in query string */ return s_req_query_string; case '#': return s_req_fragment_start; } break; case s_req_fragment_start: if (IS_URL_CHAR(ch)) { return s_req_fragment; } switch (ch) { case '?': return s_req_fragment; case '#': return s; } break; case s_req_fragment: if (IS_URL_CHAR(ch)) { return s; } switch (ch) { case '?': case '#': return s; } break; default: break; } /* We should never fall out of the switch above unless there's an error */ return s_dead; } size_t http_parser_execute (http_parser *parser, const http_parser_settings *settings, const char *data, size_t len) { char c, ch; int8_t unhex_val; const char *p = data; const char *header_field_mark = 0; const char *header_value_mark = 0; const char *url_mark = 0; const char *body_mark = 0; const char *status_mark = 0; /* We're in an error state. Don't bother doing anything. */ if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { return 0; } if (len == 0) { switch (parser->state) { case s_body_identity_eof: /* Use of CALLBACK_NOTIFY() here would erroneously return 1 byte read if * we got paused. */ CALLBACK_NOTIFY_NOADVANCE(message_complete); return 0; case s_dead: case s_start_req_or_res: case s_start_res: case s_start_req: return 0; default: SET_ERRNO(HPE_INVALID_EOF_STATE); return 1; } } if (parser->state == s_header_field) header_field_mark = data; if (parser->state == s_header_value) header_value_mark = data; switch (parser->state) { case s_req_path: case s_req_schema: case s_req_schema_slash: case s_req_schema_slash_slash: case s_req_server_start: case s_req_server: case s_req_server_with_at: case s_req_query_string_start: case s_req_query_string: case s_req_fragment_start: case s_req_fragment: url_mark = data; break; case s_res_status: status_mark = data; break; } for (p=data; p != data + len; p++) { ch = *p; if (PARSING_HEADER(parser->state)) { ++parser->nread; /* Don't allow the total size of the HTTP headers (including the status * line) to exceed HTTP_MAX_HEADER_SIZE. This check is here to protect * embedders against denial-of-service attacks where the attacker feeds * us a never-ending header that the embedder keeps buffering. * * This check is arguably the responsibility of embedders but we're doing * it on the embedder's behalf because most won't bother and this way we * make the web a little safer. HTTP_MAX_HEADER_SIZE is still far bigger * than any reasonable request or response so this should never affect * day-to-day operation. */ if (parser->nread > HTTP_MAX_HEADER_SIZE) { SET_ERRNO(HPE_HEADER_OVERFLOW); goto error; } } reexecute_byte: switch (parser->state) { case s_dead: /* this state is used after a 'Connection: close' message * the parser will error out if it reads another message */ if (ch == CR || ch == LF) break; SET_ERRNO(HPE_CLOSED_CONNECTION); goto error; case s_start_req_or_res: { if (ch == CR || ch == LF) break; parser->flags = 0; parser->content_length = ULLONG_MAX; if (ch == 'H') { parser->state = s_res_or_resp_H; CALLBACK_NOTIFY(message_begin); } else { parser->type = HTTP_REQUEST; parser->state = s_start_req; goto reexecute_byte; } break; } case s_res_or_resp_H: if (ch == 'T') { parser->type = HTTP_RESPONSE; parser->state = s_res_HT; } else { if (ch != 'E') { SET_ERRNO(HPE_INVALID_CONSTANT); goto error; } parser->type = HTTP_REQUEST; parser->method = HTTP_HEAD; parser->index = 2; parser->state = s_req_method; } break; case s_start_res: { parser->flags = 0; parser->content_length = ULLONG_MAX; switch (ch) { case 'H': parser->state = s_res_H; break; case CR: case LF: break; default: SET_ERRNO(HPE_INVALID_CONSTANT); goto error; } CALLBACK_NOTIFY(message_begin); break; } case s_res_H: STRICT_CHECK(ch != 'T'); parser->state = s_res_HT; break; case s_res_HT: STRICT_CHECK(ch != 'T'); parser->state = s_res_HTT; break; case s_res_HTT: STRICT_CHECK(ch != 'P'); parser->state = s_res_HTTP; break; case s_res_HTTP: STRICT_CHECK(ch != '/'); parser->state = s_res_first_http_major; break; case s_res_first_http_major: if (ch < '0' || ch > '9') { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_major = ch - '0'; parser->state = s_res_http_major; break; /* major HTTP version or dot */ case s_res_http_major: { if (ch == '.') { parser->state = s_res_first_http_minor; break; } if (!IS_NUM(ch)) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_major *= 10; parser->http_major += ch - '0'; if (parser->http_major > 999) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } break; } /* first digit of minor HTTP version */ case s_res_first_http_minor: if (!IS_NUM(ch)) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_minor = ch - '0'; parser->state = s_res_http_minor; break; /* minor HTTP version or end of request line */ case s_res_http_minor: { if (ch == ' ') { parser->state = s_res_first_status_code; break; } if (!IS_NUM(ch)) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_minor *= 10; parser->http_minor += ch - '0'; if (parser->http_minor > 999) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } break; } case s_res_first_status_code: { if (!IS_NUM(ch)) { if (ch == ' ') { break; } SET_ERRNO(HPE_INVALID_STATUS); goto error; } parser->status_code = ch - '0'; parser->state = s_res_status_code; break; } case s_res_status_code: { if (!IS_NUM(ch)) { switch (ch) { case ' ': parser->state = s_res_status_start; break; case CR: parser->state = s_res_line_almost_done; break; case LF: parser->state = s_header_field_start; break; default: SET_ERRNO(HPE_INVALID_STATUS); goto error; } break; } parser->status_code *= 10; parser->status_code += ch - '0'; if (parser->status_code > 999) { SET_ERRNO(HPE_INVALID_STATUS); goto error; } break; } case s_res_status_start: { if (ch == CR) { parser->state = s_res_line_almost_done; break; } if (ch == LF) { parser->state = s_header_field_start; break; } MARK(status); parser->state = s_res_status; parser->index = 0; break; } case s_res_status: if (ch == CR) { parser->state = s_res_line_almost_done; CALLBACK_DATA(status); break; } if (ch == LF) { parser->state = s_header_field_start; CALLBACK_DATA(status); break; } break; case s_res_line_almost_done: STRICT_CHECK(ch != LF); parser->state = s_header_field_start; break; case s_start_req: { if (ch == CR || ch == LF) break; parser->flags = 0; parser->content_length = ULLONG_MAX; if (!IS_ALPHA(ch)) { SET_ERRNO(HPE_INVALID_METHOD); goto error; } parser->method = (enum http_method) 0; parser->index = 1; switch (ch) { case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; case 'D': parser->method = HTTP_DELETE; break; case 'G': parser->method = HTTP_GET; break; case 'H': parser->method = HTTP_HEAD; break; case 'L': parser->method = HTTP_LOCK; break; case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH */ break; case 'N': parser->method = HTTP_NOTIFY; break; case 'O': parser->method = HTTP_OPTIONS; break; case 'P': parser->method = HTTP_POST; /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ break; case 'R': parser->method = HTTP_REPORT; break; case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH */ break; case 'T': parser->method = HTTP_TRACE; break; case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE */ break; default: SET_ERRNO(HPE_INVALID_METHOD); goto error; } parser->state = s_req_method; CALLBACK_NOTIFY(message_begin); break; } case s_req_method: { const char *matcher; if (ch == '\0') { SET_ERRNO(HPE_INVALID_METHOD); goto error; } matcher = method_strings[parser->method]; if (ch == ' ' && matcher[parser->index] == '\0') { parser->state = s_req_spaces_before_url; } else if (ch == matcher[parser->index]) { ; /* nada */ } else if (parser->method == HTTP_CONNECT) { if (parser->index == 1 && ch == 'H') { parser->method = HTTP_CHECKOUT; } else if (parser->index == 2 && ch == 'P') { parser->method = HTTP_COPY; } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } } else if (parser->method == HTTP_MKCOL) { if (parser->index == 1 && ch == 'O') { parser->method = HTTP_MOVE; } else if (parser->index == 1 && ch == 'E') { parser->method = HTTP_MERGE; } else if (parser->index == 1 && ch == '-') { parser->method = HTTP_MSEARCH; } else if (parser->index == 2 && ch == 'A') { parser->method = HTTP_MKACTIVITY; } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } } else if (parser->method == HTTP_SUBSCRIBE) { if (parser->index == 1 && ch == 'E') { parser->method = HTTP_SEARCH; } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } } else if (parser->index == 1 && parser->method == HTTP_POST) { if (ch == 'R') { parser->method = HTTP_PROPFIND; /* or HTTP_PROPPATCH */ } else if (ch == 'U') { parser->method = HTTP_PUT; /* or HTTP_PURGE */ } else if (ch == 'A') { parser->method = HTTP_PATCH; } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } } else if (parser->index == 2) { if (parser->method == HTTP_PUT) { if (ch == 'R') { parser->method = HTTP_PURGE; } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } } else if (parser->method == HTTP_UNLOCK) { if (ch == 'S') { parser->method = HTTP_UNSUBSCRIBE; } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } } else if (parser->index == 4 && parser->method == HTTP_PROPFIND && ch == 'P') { parser->method = HTTP_PROPPATCH; } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } ++parser->index; break; } case s_req_spaces_before_url: { if (ch == ' ') break; MARK(url); if (parser->method == HTTP_CONNECT) { parser->state = s_req_server_start; } parser->state = parse_url_char((enum state)parser->state, ch); if (parser->state == s_dead) { SET_ERRNO(HPE_INVALID_URL); goto error; } break; } case s_req_schema: case s_req_schema_slash: case s_req_schema_slash_slash: case s_req_server_start: { switch (ch) { /* No whitespace allowed here */ case ' ': case CR: case LF: SET_ERRNO(HPE_INVALID_URL); goto error; default: parser->state = parse_url_char((enum state)parser->state, ch); if (parser->state == s_dead) { SET_ERRNO(HPE_INVALID_URL); goto error; } } break; } case s_req_server: case s_req_server_with_at: case s_req_path: case s_req_query_string_start: case s_req_query_string: case s_req_fragment_start: case s_req_fragment: { switch (ch) { case ' ': parser->state = s_req_http_start; CALLBACK_DATA(url); break; case CR: case LF: parser->http_major = 0; parser->http_minor = 9; parser->state = (ch == CR) ? s_req_line_almost_done : s_header_field_start; CALLBACK_DATA(url); break; default: parser->state = parse_url_char((enum state)parser->state, ch); if (parser->state == s_dead) { SET_ERRNO(HPE_INVALID_URL); goto error; } } break; } case s_req_http_start: switch (ch) { case 'H': parser->state = s_req_http_H; break; case ' ': break; default: SET_ERRNO(HPE_INVALID_CONSTANT); goto error; } break; case s_req_http_H: STRICT_CHECK(ch != 'T'); parser->state = s_req_http_HT; break; case s_req_http_HT: STRICT_CHECK(ch != 'T'); parser->state = s_req_http_HTT; break; case s_req_http_HTT: STRICT_CHECK(ch != 'P'); parser->state = s_req_http_HTTP; break; case s_req_http_HTTP: STRICT_CHECK(ch != '/'); parser->state = s_req_first_http_major; break; /* first digit of major HTTP version */ case s_req_first_http_major: if (ch < '1' || ch > '9') { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_major = ch - '0'; parser->state = s_req_http_major; break; /* major HTTP version or dot */ case s_req_http_major: { if (ch == '.') { parser->state = s_req_first_http_minor; break; } if (!IS_NUM(ch)) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_major *= 10; parser->http_major += ch - '0'; if (parser->http_major > 999) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } break; } /* first digit of minor HTTP version */ case s_req_first_http_minor: if (!IS_NUM(ch)) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_minor = ch - '0'; parser->state = s_req_http_minor; break; /* minor HTTP version or end of request line */ case s_req_http_minor: { if (ch == CR) { parser->state = s_req_line_almost_done; break; } if (ch == LF) { parser->state = s_header_field_start; break; } /* XXX allow spaces after digit? */ if (!IS_NUM(ch)) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_minor *= 10; parser->http_minor += ch - '0'; if (parser->http_minor > 999) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } break; } /* end of request line */ case s_req_line_almost_done: { if (ch != LF) { SET_ERRNO(HPE_LF_EXPECTED); goto error; } parser->state = s_header_field_start; break; } case s_header_field_start: { if (ch == CR) { parser->state = s_headers_almost_done; break; } if (ch == LF) { /* they might be just sending \n instead of \r\n so this would be * the second \n to denote the end of headers*/ parser->state = s_headers_almost_done; goto reexecute_byte; } c = TOKEN(ch); if (!c) { SET_ERRNO(HPE_INVALID_HEADER_TOKEN); goto error; } MARK(header_field); parser->index = 0; parser->state = s_header_field; switch (c) { case 'c': parser->header_state = h_C; break; case 'p': parser->header_state = h_matching_proxy_connection; break; case 't': parser->header_state = h_matching_transfer_encoding; break; case 'u': parser->header_state = h_matching_upgrade; break; default: parser->header_state = h_general; break; } break; } case s_header_field: { c = TOKEN(ch); if (c) { switch (parser->header_state) { case h_general: break; case h_C: parser->index++; parser->header_state = (c == 'o' ? h_CO : h_general); break; case h_CO: parser->index++; parser->header_state = (c == 'n' ? h_CON : h_general); break; case h_CON: parser->index++; switch (c) { case 'n': parser->header_state = h_matching_connection; break; case 't': parser->header_state = h_matching_content_length; break; default: parser->header_state = h_general; break; } break; /* connection */ case h_matching_connection: parser->index++; if (parser->index > sizeof(CONNECTION)-1 || c != CONNECTION[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(CONNECTION)-2) { parser->header_state = h_connection; } break; /* proxy-connection */ case h_matching_proxy_connection: parser->index++; if (parser->index > sizeof(PROXY_CONNECTION)-1 || c != PROXY_CONNECTION[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(PROXY_CONNECTION)-2) { parser->header_state = h_connection; } break; /* content-length */ case h_matching_content_length: parser->index++; if (parser->index > sizeof(CONTENT_LENGTH)-1 || c != CONTENT_LENGTH[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(CONTENT_LENGTH)-2) { parser->header_state = h_content_length; } break; /* transfer-encoding */ case h_matching_transfer_encoding: parser->index++; if (parser->index > sizeof(TRANSFER_ENCODING)-1 || c != TRANSFER_ENCODING[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(TRANSFER_ENCODING)-2) { parser->header_state = h_transfer_encoding; } break; /* upgrade */ case h_matching_upgrade: parser->index++; if (parser->index > sizeof(UPGRADE)-1 || c != UPGRADE[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(UPGRADE)-2) { parser->header_state = h_upgrade; } break; case h_connection: case h_content_length: case h_transfer_encoding: case h_upgrade: if (ch != ' ') parser->header_state = h_general; break; default: assert(0 && "Unknown header_state"); break; } break; } if (ch == ':') { parser->state = s_header_value_start; CALLBACK_DATA(header_field); break; } if (ch == CR) { parser->state = s_header_almost_done; CALLBACK_DATA(header_field); break; } if (ch == LF) { parser->state = s_header_field_start; CALLBACK_DATA(header_field); break; } SET_ERRNO(HPE_INVALID_HEADER_TOKEN); goto error; } case s_header_value_start: { if (ch == ' ' || ch == '\t') break; MARK(header_value); parser->state = s_header_value; parser->index = 0; if (ch == CR) { parser->header_state = h_general; parser->state = s_header_almost_done; CALLBACK_DATA(header_value); break; } if (ch == LF) { parser->state = s_header_field_start; CALLBACK_DATA(header_value); break; } c = LOWER(ch); switch (parser->header_state) { case h_upgrade: parser->flags |= F_UPGRADE; parser->header_state = h_general; break; case h_transfer_encoding: /* looking for 'Transfer-Encoding: chunked' */ if ('c' == c) { parser->header_state = h_matching_transfer_encoding_chunked; } else { parser->header_state = h_general; } break; case h_content_length: if (!IS_NUM(ch)) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); goto error; } parser->content_length = ch - '0'; break; case h_connection: /* looking for 'Connection: keep-alive' */ if (c == 'k') { parser->header_state = h_matching_connection_keep_alive; /* looking for 'Connection: close' */ } else if (c == 'c') { parser->header_state = h_matching_connection_close; } else { parser->header_state = h_general; } break; default: parser->header_state = h_general; break; } break; } case s_header_value: { if (ch == CR) { parser->state = s_header_almost_done; CALLBACK_DATA(header_value); break; } if (ch == LF) { parser->state = s_header_almost_done; CALLBACK_DATA_NOADVANCE(header_value); goto reexecute_byte; } c = LOWER(ch); switch (parser->header_state) { case h_general: break; case h_connection: case h_transfer_encoding: assert(0 && "Shouldn't get here."); break; case h_content_length: { uint64_t t; if (ch == ' ') break; if (!IS_NUM(ch)) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); goto error; } t = parser->content_length; t *= 10; t += ch - '0'; /* Overflow? Test against a conservative limit for simplicity. */ if ((ULLONG_MAX - 10) / 10 < parser->content_length) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); goto error; } parser->content_length = t; break; } /* Transfer-Encoding: chunked */ case h_matching_transfer_encoding_chunked: parser->index++; if (parser->index > sizeof(CHUNKED)-1 || c != CHUNKED[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(CHUNKED)-2) { parser->header_state = h_transfer_encoding_chunked; } break; /* looking for 'Connection: keep-alive' */ case h_matching_connection_keep_alive: parser->index++; if (parser->index > sizeof(KEEP_ALIVE)-1 || c != KEEP_ALIVE[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(KEEP_ALIVE)-2) { parser->header_state = h_connection_keep_alive; } break; /* looking for 'Connection: close' */ case h_matching_connection_close: parser->index++; if (parser->index > sizeof(CLOSE)-1 || c != CLOSE[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(CLOSE)-2) { parser->header_state = h_connection_close; } break; case h_transfer_encoding_chunked: case h_connection_keep_alive: case h_connection_close: if (ch != ' ') parser->header_state = h_general; break; default: parser->state = s_header_value; parser->header_state = h_general; break; } break; } case s_header_almost_done: { STRICT_CHECK(ch != LF); parser->state = s_header_value_lws; switch (parser->header_state) { case h_connection_keep_alive: parser->flags |= F_CONNECTION_KEEP_ALIVE; break; case h_connection_close: parser->flags |= F_CONNECTION_CLOSE; break; case h_transfer_encoding_chunked: parser->flags |= F_CHUNKED; break; default: break; } break; } case s_header_value_lws: { if (ch == ' ' || ch == '\t') parser->state = s_header_value_start; else { parser->state = s_header_field_start; goto reexecute_byte; } break; } case s_headers_almost_done: { STRICT_CHECK(ch != LF); if (parser->flags & F_TRAILING) { /* End of a chunked request */ parser->state = NEW_MESSAGE(); CALLBACK_NOTIFY(message_complete); break; } parser->state = s_headers_done; /* Set this here so that on_headers_complete() callbacks can see it */ parser->upgrade = (parser->flags & F_UPGRADE || parser->method == HTTP_CONNECT); /* Here we call the headers_complete callback. This is somewhat * different than other callbacks because if the user returns 1, we * will interpret that as saying that this message has no body. This * is needed for the annoying case of recieving a response to a HEAD * request. * * We'd like to use CALLBACK_NOTIFY_NOADVANCE() here but we cannot, so * we have to simulate it by handling a change in errno below. */ if (settings->on_headers_complete) { switch (settings->on_headers_complete(parser)) { case 0: break; case 1: parser->flags |= F_SKIPBODY; break; default: SET_ERRNO(HPE_CB_headers_complete); return p - data; /* Error */ } } if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { return p - data; } goto reexecute_byte; } case s_headers_done: { STRICT_CHECK(ch != LF); parser->nread = 0; /* Exit, the rest of the connect is in a different protocol. */ if (parser->upgrade) { parser->state = NEW_MESSAGE(); CALLBACK_NOTIFY(message_complete); return (p - data) + 1; } if (parser->flags & F_SKIPBODY) { parser->state = NEW_MESSAGE(); CALLBACK_NOTIFY(message_complete); } else if (parser->flags & F_CHUNKED) { /* chunked encoding - ignore Content-Length header */ parser->state = s_chunk_size_start; } else { if (parser->content_length == 0) { /* Content-Length header given but zero: Content-Length: 0\r\n */ parser->state = NEW_MESSAGE(); CALLBACK_NOTIFY(message_complete); } else if (parser->content_length != ULLONG_MAX) { /* Content-Length header given and non-zero */ parser->state = s_body_identity; } else { if (parser->type == HTTP_REQUEST || !http_message_needs_eof(parser)) { /* Assume content-length 0 - read the next */ parser->state = NEW_MESSAGE(); CALLBACK_NOTIFY(message_complete); } else { /* Read body until EOF */ parser->state = s_body_identity_eof; } } } break; } case s_body_identity: { uint64_t to_read = MIN(parser->content_length, (uint64_t) ((data + len) - p)); assert(parser->content_length != 0 && parser->content_length != ULLONG_MAX); /* The difference between advancing content_length and p is because * the latter will automaticaly advance on the next loop iteration. * Further, if content_length ends up at 0, we want to see the last * byte again for our message complete callback. */ MARK(body); parser->content_length -= to_read; p += to_read - 1; if (parser->content_length == 0) { parser->state = s_message_done; /* Mimic CALLBACK_DATA_NOADVANCE() but with one extra byte. * * The alternative to doing this is to wait for the next byte to * trigger the data callback, just as in every other case. The * problem with this is that this makes it difficult for the test * harness to distinguish between complete-on-EOF and * complete-on-length. It's not clear that this distinction is * important for applications, but let's keep it for now. */ CALLBACK_DATA_(body, p - body_mark + 1, p - data); goto reexecute_byte; } break; } /* read until EOF */ case s_body_identity_eof: MARK(body); p = data + len - 1; break; case s_message_done: parser->state = NEW_MESSAGE(); CALLBACK_NOTIFY(message_complete); break; case s_chunk_size_start: { assert(parser->nread == 1); assert(parser->flags & F_CHUNKED); unhex_val = unhex[(unsigned char)ch]; if (unhex_val == -1) { SET_ERRNO(HPE_INVALID_CHUNK_SIZE); goto error; } parser->content_length = unhex_val; parser->state = s_chunk_size; break; } case s_chunk_size: { uint64_t t; assert(parser->flags & F_CHUNKED); if (ch == CR) { parser->state = s_chunk_size_almost_done; break; } unhex_val = unhex[(unsigned char)ch]; if (unhex_val == -1) { if (ch == ';' || ch == ' ') { parser->state = s_chunk_parameters; break; } SET_ERRNO(HPE_INVALID_CHUNK_SIZE); goto error; } t = parser->content_length; t *= 16; t += unhex_val; /* Overflow? Test against a conservative limit for simplicity. */ if ((ULLONG_MAX - 16) / 16 < parser->content_length) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); goto error; } parser->content_length = t; break; } case s_chunk_parameters: { assert(parser->flags & F_CHUNKED); /* just ignore this shit. TODO check for overflow */ if (ch == CR) { parser->state = s_chunk_size_almost_done; break; } break; } case s_chunk_size_almost_done: { assert(parser->flags & F_CHUNKED); STRICT_CHECK(ch != LF); parser->nread = 0; if (parser->content_length == 0) { parser->flags |= F_TRAILING; parser->state = s_header_field_start; } else { parser->state = s_chunk_data; } break; } case s_chunk_data: { uint64_t to_read = MIN(parser->content_length, (uint64_t) ((data + len) - p)); assert(parser->flags & F_CHUNKED); assert(parser->content_length != 0 && parser->content_length != ULLONG_MAX); /* See the explanation in s_body_identity for why the content * length and data pointers are managed this way. */ MARK(body); parser->content_length -= to_read; p += to_read - 1; if (parser->content_length == 0) { parser->state = s_chunk_data_almost_done; } break; } case s_chunk_data_almost_done: assert(parser->flags & F_CHUNKED); assert(parser->content_length == 0); STRICT_CHECK(ch != CR); parser->state = s_chunk_data_done; CALLBACK_DATA(body); break; case s_chunk_data_done: assert(parser->flags & F_CHUNKED); STRICT_CHECK(ch != LF); parser->nread = 0; parser->state = s_chunk_size_start; break; default: assert(0 && "unhandled state"); SET_ERRNO(HPE_INVALID_INTERNAL_STATE); goto error; } } /* Run callbacks for any marks that we have leftover after we ran our of * bytes. There should be at most one of these set, so it's OK to invoke * them in series (unset marks will not result in callbacks). * * We use the NOADVANCE() variety of callbacks here because 'p' has already * overflowed 'data' and this allows us to correct for the off-by-one that * we'd otherwise have (since CALLBACK_DATA() is meant to be run with a 'p' * value that's in-bounds). */ assert(((header_field_mark ? 1 : 0) + (header_value_mark ? 1 : 0) + (url_mark ? 1 : 0) + (body_mark ? 1 : 0) + (status_mark ? 1 : 0)) <= 1); CALLBACK_DATA_NOADVANCE(header_field); CALLBACK_DATA_NOADVANCE(header_value); CALLBACK_DATA_NOADVANCE(url); CALLBACK_DATA_NOADVANCE(body); CALLBACK_DATA_NOADVANCE(status); return len; error: if (HTTP_PARSER_ERRNO(parser) == HPE_OK) { SET_ERRNO(HPE_UNKNOWN); } return (p - data); } /* Does the parser need to see an EOF to find the end of the message? */ int http_message_needs_eof (const http_parser *parser) { if (parser->type == HTTP_REQUEST) { return 0; } /* See RFC 2616 section 4.4 */ if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */ parser->status_code == 204 || /* No Content */ parser->status_code == 304 || /* Not Modified */ parser->flags & F_SKIPBODY) { /* response to a HEAD request */ return 0; } if ((parser->flags & F_CHUNKED) || parser->content_length != ULLONG_MAX) { return 0; } return 1; } int http_should_keep_alive (const http_parser *parser) { if (parser->http_major > 0 && parser->http_minor > 0) { /* HTTP/1.1 */ if (parser->flags & F_CONNECTION_CLOSE) { return 0; } } else { /* HTTP/1.0 or earlier */ if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) { return 0; } } return !http_message_needs_eof(parser); } const char * http_method_str (enum http_method m) { return ELEM_AT(method_strings, m, ""); } void http_parser_init (http_parser *parser, enum http_parser_type t) { void *data = parser->data; /* preserve application data */ memset(parser, 0, sizeof(*parser)); parser->data = data; parser->type = t; parser->state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res)); parser->http_errno = HPE_OK; } const char * http_errno_name(enum http_errno err) { assert(err < (sizeof(http_strerror_tab)/sizeof(http_strerror_tab[0]))); return http_strerror_tab[err].name; } const char * http_errno_description(enum http_errno err) { assert(err < (sizeof(http_strerror_tab)/sizeof(http_strerror_tab[0]))); return http_strerror_tab[err].description; } static enum http_host_state http_parse_host_char(enum http_host_state s, const char ch) { switch(s) { case s_http_userinfo: case s_http_userinfo_start: if (ch == '@') { return s_http_host_start; } if (IS_USERINFO_CHAR(ch)) { return s_http_userinfo; } break; case s_http_host_start: if (ch == '[') { return s_http_host_v6_start; } if (IS_HOST_CHAR(ch)) { return s_http_host; } break; case s_http_host: if (IS_HOST_CHAR(ch)) { return s_http_host; } /* FALLTHROUGH */ case s_http_host_v6_end: if (ch == ':') { return s_http_host_port_start; } break; case s_http_host_v6: if (ch == ']') { return s_http_host_v6_end; } /* FALLTHROUGH */ case s_http_host_v6_start: if (IS_HEX(ch) || ch == ':' || ch == '.') { return s_http_host_v6; } break; case s_http_host_port: case s_http_host_port_start: if (IS_NUM(ch)) { return s_http_host_port; } break; default: break; } return s_http_host_dead; } static int http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { enum http_host_state s; const char *p; size_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len; u->field_data[UF_HOST].len = 0; s = found_at ? s_http_userinfo_start : s_http_host_start; for (p = buf + u->field_data[UF_HOST].off; p < buf + buflen; p++) { enum http_host_state new_s = http_parse_host_char(s, *p); if (new_s == s_http_host_dead) { return 1; } switch(new_s) { case s_http_host: if (s != s_http_host) { u->field_data[UF_HOST].off = p - buf; } u->field_data[UF_HOST].len++; break; case s_http_host_v6: if (s != s_http_host_v6) { u->field_data[UF_HOST].off = p - buf; } u->field_data[UF_HOST].len++; break; case s_http_host_port: if (s != s_http_host_port) { u->field_data[UF_PORT].off = p - buf; u->field_data[UF_PORT].len = 0; u->field_set |= (1 << UF_PORT); } u->field_data[UF_PORT].len++; break; case s_http_userinfo: if (s != s_http_userinfo) { u->field_data[UF_USERINFO].off = p - buf ; u->field_data[UF_USERINFO].len = 0; u->field_set |= (1 << UF_USERINFO); } u->field_data[UF_USERINFO].len++; break; default: break; } s = new_s; } /* Make sure we don't end somewhere unexpected */ switch (s) { case s_http_host_start: case s_http_host_v6_start: case s_http_host_v6: case s_http_host_port_start: case s_http_userinfo: case s_http_userinfo_start: return 1; default: break; } return 0; } int http_parser_parse_url(const char *buf, size_t buflen, int is_connect, struct http_parser_url *u) { enum state s; const char *p; enum http_parser_url_fields uf, old_uf; int found_at = 0; u->port = u->field_set = 0; s = is_connect ? s_req_server_start : s_req_spaces_before_url; uf = old_uf = UF_MAX; for (p = buf; p < buf + buflen; p++) { s = parse_url_char(s, *p); /* Figure out the next field that we're operating on */ switch (s) { case s_dead: return 1; /* Skip delimeters */ case s_req_schema_slash: case s_req_schema_slash_slash: case s_req_server_start: case s_req_query_string_start: case s_req_fragment_start: continue; case s_req_schema: uf = UF_SCHEMA; break; case s_req_server_with_at: found_at = 1; /* FALLTROUGH */ case s_req_server: uf = UF_HOST; break; case s_req_path: uf = UF_PATH; break; case s_req_query_string: uf = UF_QUERY; break; case s_req_fragment: uf = UF_FRAGMENT; break; default: assert(!"Unexpected state"); return 1; } /* Nothing's changed; soldier on */ if (uf == old_uf) { u->field_data[uf].len++; continue; } u->field_data[uf].off = p - buf; u->field_data[uf].len = 1; u->field_set |= (1 << uf); old_uf = uf; } /* host must be present if there is a schema */ /* parsing http:///toto will fail */ if ((u->field_set & ((1 << UF_SCHEMA) | (1 << UF_HOST))) != 0) { if (http_parse_host(buf, u, found_at) != 0) { return 1; } } /* CONNECT requests can only contain "hostname:port" */ if (is_connect && u->field_set != ((1 << UF_HOST)|(1 << UF_PORT))) { return 1; } if (u->field_set & (1 << UF_PORT)) { /* Don't bother with endp; we've already validated the string */ unsigned long v = strtoul(buf + u->field_data[UF_PORT].off, NULL, 10); /* Ports have a max value of 2^16 */ if (v > 0xffff) { return 1; } u->port = (uint16_t) v; } return 0; } void http_parser_pause(http_parser *parser, int paused) { /* Users should only be pausing/unpausing a parser that is not in an error * state. In non-debug builds, there's not much that we can do about this * other than ignore it. */ if (HTTP_PARSER_ERRNO(parser) == HPE_OK || HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) { SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK); } else { assert(0 && "Attempting to pause parser in error state"); } } int http_body_is_final(const struct http_parser *parser) { return parser->state == s_message_done; } unsigned long http_parser_version(void) { return HTTP_PARSER_VERSION_MAJOR * 0x10000 | HTTP_PARSER_VERSION_MINOR * 0x00100 | HTTP_PARSER_VERSION_PATCH * 0x00001; } ================================================ FILE: src/http_parser.h ================================================ /* Copyright Joyent, Inc. and other Node contributors. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #ifndef http_parser_h #define http_parser_h #ifdef __cplusplus extern "C" { #endif /* Also update SONAME in the Makefile whenever you change these. */ #define HTTP_PARSER_VERSION_MAJOR 2 #define HTTP_PARSER_VERSION_MINOR 2 #define HTTP_PARSER_VERSION_PATCH 0 #include #if defined(_WIN32) && !defined(__MINGW32__) && (!defined(_MSC_VER) || _MSC_VER<1600) #include #include typedef __int8 int8_t; typedef unsigned __int8 uint8_t; typedef __int16 int16_t; typedef unsigned __int16 uint16_t; typedef __int32 int32_t; typedef unsigned __int32 uint32_t; typedef __int64 int64_t; typedef unsigned __int64 uint64_t; #else #include #endif /* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run * faster */ #ifndef HTTP_PARSER_STRICT # define HTTP_PARSER_STRICT 1 #endif /* Maximium header size allowed */ #define HTTP_MAX_HEADER_SIZE (80*1024) typedef struct http_parser http_parser; typedef struct http_parser_settings http_parser_settings; /* Callbacks should return non-zero to indicate an error. The parser will * then halt execution. * * The one exception is on_headers_complete. In a HTTP_RESPONSE parser * returning '1' from on_headers_complete will tell the parser that it * should not expect a body. This is used when receiving a response to a * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: * chunked' headers that indicate the presence of a body. * * http_data_cb does not return data chunks. It will be call arbitrarally * many times for each string. E.G. you might get 10 callbacks for "on_url" * each providing just a few characters more data. */ typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); typedef int (*http_cb) (http_parser*); /* Request Methods */ #define HTTP_METHOD_MAP(XX) \ XX(0, DELETE, DELETE) \ XX(1, GET, GET) \ XX(2, HEAD, HEAD) \ XX(3, POST, POST) \ XX(4, PUT, PUT) \ /* pathological */ \ XX(5, CONNECT, CONNECT) \ XX(6, OPTIONS, OPTIONS) \ XX(7, TRACE, TRACE) \ /* webdav */ \ XX(8, COPY, COPY) \ XX(9, LOCK, LOCK) \ XX(10, MKCOL, MKCOL) \ XX(11, MOVE, MOVE) \ XX(12, PROPFIND, PROPFIND) \ XX(13, PROPPATCH, PROPPATCH) \ XX(14, SEARCH, SEARCH) \ XX(15, UNLOCK, UNLOCK) \ /* subversion */ \ XX(16, REPORT, REPORT) \ XX(17, MKACTIVITY, MKACTIVITY) \ XX(18, CHECKOUT, CHECKOUT) \ XX(19, MERGE, MERGE) \ /* upnp */ \ XX(20, MSEARCH, M-SEARCH) \ XX(21, NOTIFY, NOTIFY) \ XX(22, SUBSCRIBE, SUBSCRIBE) \ XX(23, UNSUBSCRIBE, UNSUBSCRIBE) \ /* RFC-5789 */ \ XX(24, PATCH, PATCH) \ XX(25, PURGE, PURGE) \ enum http_method { #define XX(num, name, string) HTTP_##name = num, HTTP_METHOD_MAP(XX) #undef XX }; enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH }; /* Flag values for http_parser.flags field */ enum flags { F_CHUNKED = 1 << 0 , F_CONNECTION_KEEP_ALIVE = 1 << 1 , F_CONNECTION_CLOSE = 1 << 2 , F_TRAILING = 1 << 3 , F_UPGRADE = 1 << 4 , F_SKIPBODY = 1 << 5 }; /* Map for errno-related constants * * The provided argument should be a macro that takes 2 arguments. */ #define HTTP_ERRNO_MAP(XX) \ /* No error */ \ XX(OK, "success") \ \ /* Callback-related errors */ \ XX(CB_message_begin, "the on_message_begin callback failed") \ XX(CB_url, "the on_url callback failed") \ XX(CB_header_field, "the on_header_field callback failed") \ XX(CB_header_value, "the on_header_value callback failed") \ XX(CB_headers_complete, "the on_headers_complete callback failed") \ XX(CB_body, "the on_body callback failed") \ XX(CB_message_complete, "the on_message_complete callback failed") \ XX(CB_status, "the on_status callback failed") \ \ /* Parsing-related errors */ \ XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \ XX(HEADER_OVERFLOW, \ "too many header bytes seen; overflow detected") \ XX(CLOSED_CONNECTION, \ "data received after completed connection: close message") \ XX(INVALID_VERSION, "invalid HTTP version") \ XX(INVALID_STATUS, "invalid HTTP status code") \ XX(INVALID_METHOD, "invalid HTTP method") \ XX(INVALID_URL, "invalid URL") \ XX(INVALID_HOST, "invalid host") \ XX(INVALID_PORT, "invalid port") \ XX(INVALID_PATH, "invalid path") \ XX(INVALID_QUERY_STRING, "invalid query string") \ XX(INVALID_FRAGMENT, "invalid fragment") \ XX(LF_EXPECTED, "LF character expected") \ XX(INVALID_HEADER_TOKEN, "invalid character in header") \ XX(INVALID_CONTENT_LENGTH, \ "invalid character in content-length header") \ XX(INVALID_CHUNK_SIZE, \ "invalid character in chunk size header") \ XX(INVALID_CONSTANT, "invalid constant string") \ XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\ XX(STRICT, "strict mode assertion failed") \ XX(PAUSED, "parser is paused") \ XX(UNKNOWN, "an unknown error occurred") /* Define HPE_* values for each errno value above */ #define HTTP_ERRNO_GEN(n, s) HPE_##n, enum http_errno { HTTP_ERRNO_MAP(HTTP_ERRNO_GEN) }; #undef HTTP_ERRNO_GEN /* Get an http_errno value from an http_parser */ #define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno) struct http_parser { /** PRIVATE **/ unsigned int type : 2; /* enum http_parser_type */ unsigned int flags : 6; /* F_* values from 'flags' enum; semi-public */ unsigned int state : 8; /* enum state from http_parser.c */ unsigned int header_state : 8; /* enum header_state from http_parser.c */ unsigned int index : 8; /* index into current matcher */ uint32_t nread; /* # bytes read in various scenarios */ uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */ /** READ-ONLY **/ unsigned short http_major; unsigned short http_minor; unsigned int status_code : 16; /* responses only */ unsigned int method : 8; /* requests only */ unsigned int http_errno : 7; /* 1 = Upgrade header was present and the parser has exited because of that. * 0 = No upgrade header present. * Should be checked when http_parser_execute() returns in addition to * error checking. */ unsigned int upgrade : 1; /** PUBLIC **/ void *data; /* A pointer to get hook to the "connection" or "socket" object */ }; struct http_parser_settings { http_cb on_message_begin; http_data_cb on_url; http_data_cb on_status; http_data_cb on_header_field; http_data_cb on_header_value; http_cb on_headers_complete; http_data_cb on_body; http_cb on_message_complete; }; enum http_parser_url_fields { UF_SCHEMA = 0 , UF_HOST = 1 , UF_PORT = 2 , UF_PATH = 3 , UF_QUERY = 4 , UF_FRAGMENT = 5 , UF_USERINFO = 6 , UF_MAX = 7 }; /* Result structure for http_parser_parse_url(). * * Callers should index into field_data[] with UF_* values iff field_set * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and * because we probably have padding left over), we convert any port to * a uint16_t. */ struct http_parser_url { uint16_t field_set; /* Bitmask of (1 << UF_*) values */ uint16_t port; /* Converted UF_PORT string */ struct { uint16_t off; /* Offset into buffer in which field starts */ uint16_t len; /* Length of run in buffer */ } field_data[UF_MAX]; }; /* Returns the library version. Bits 16-23 contain the major version number, * bits 8-15 the minor version number and bits 0-7 the patch level. * Usage example: * * unsigned long version = http_parser_version(); * unsigned major = (version >> 16) & 255; * unsigned minor = (version >> 8) & 255; * unsigned patch = version & 255; * printf("http_parser v%u.%u.%u\n", major, minor, version); */ unsigned long http_parser_version(void); void http_parser_init(http_parser *parser, enum http_parser_type type); size_t http_parser_execute(http_parser *parser, const http_parser_settings *settings, const char *data, size_t len); /* If http_should_keep_alive() in the on_headers_complete or * on_message_complete callback returns 0, then this should be * the last message on the connection. * If you are the server, respond with the "Connection: close" header. * If you are the client, close the connection. */ int http_should_keep_alive(const http_parser *parser); /* Returns a string version of the HTTP method. */ const char *http_method_str(enum http_method m); /* Return a string name of the given error */ const char *http_errno_name(enum http_errno err); /* Return a string description of the given error */ const char *http_errno_description(enum http_errno err); /* Parse a URL; return nonzero on failure */ int http_parser_parse_url(const char *buf, size_t buflen, int is_connect, struct http_parser_url *u); /* Pause or un-pause the parser; a nonzero value pauses */ void http_parser_pause(http_parser *parser, int paused); /* Checks if this is the final chunk of the body. */ int http_body_is_final(const http_parser *parser); #ifdef __cplusplus } #endif #endif ================================================ FILE: src/lfs.c ================================================ /* ** LuaFileSystem ** Copyright Kepler Project 2003 (http://www.keplerproject.org/luafilesystem) ** ** File system manipulation library. ** This library offers these functions: ** lfs.attributes (filepath [, attributename]) ** lfs.chdir (path) ** lfs.currentdir () ** lfs.dir (path) ** lfs.lock (fh, mode) ** lfs.lock_dir (path) ** lfs.mkdir (path) ** lfs.rmdir (path) ** lfs.setmode (filepath, mode) ** lfs.symlinkattributes (filepath [, attributename]) -- thanks to Sam Roberts ** lfs.touch (filepath [, atime [, mtime]]) ** lfs.unlock (fh) ** ** $Id: lfs.c,v 1.61 2009/07/04 02:10:16 mascarenhas Exp $ */ #ifndef LFS_DO_NOT_USE_LARGE_FILE #ifndef _WIN32 #ifndef _AIX #define _FILE_OFFSET_BITS 64 /* Linux, Solaris and HP-UX */ #else #define _LARGE_FILES 1 /* AIX */ #endif #endif #endif #ifndef LFS_DO_NOT_USE_LARGE_FILE #define _LARGEFILE64_SOURCE #endif #include #include #include #include #include #include #ifdef _WIN32 #include #include #include #include #ifdef __BORLANDC__ #include #else #include #endif #include #else #include #include #include #include #include #endif #include #include #include #include "lfs.h" #define LFS_VERSION "1.6.2" #define LFS_LIBNAME "lfs" #if LUA_VERSION_NUM < 502 # define luaL_newlib(L,l) (lua_newtable(L), luaL_register(L,NULL,l)) #endif /* Define 'strerror' for systems that do not implement it */ #ifdef NO_STRERROR #define strerror(_) "System unable to describe the error" #endif /* Define 'getcwd' for systems that do not implement it */ #ifdef NO_GETCWD #define getcwd(p,s) NULL #define getcwd_error "Function 'getcwd' not provided by system" #else #define getcwd_error strerror(errno) #ifdef _WIN32 /* MAX_PATH seems to be 260. Seems kind of small. Is there a better one? */ #define LFS_MAXPATHLEN MAX_PATH #else /* For MAXPATHLEN: */ #include #define LFS_MAXPATHLEN MAXPATHLEN #endif #endif #define DIR_METATABLE "directory metatable" typedef struct dir_data { int closed; #ifdef _WIN32 intptr_t hFile; char pattern[MAX_PATH+1]; #else DIR *dir; #endif } dir_data; #define LOCK_METATABLE "lock metatable" #ifdef _WIN32 #ifdef __BORLANDC__ #define lfs_setmode(L,file,m) ((void)L, setmode(_fileno(file), m)) #define STAT_STRUCT struct stati64 #else #define lfs_setmode(L,file,m) ((void)L, _setmode(_fileno(file), m)) #define STAT_STRUCT struct _stati64 #endif #define STAT_FUNC _stati64 #define LSTAT_FUNC STAT_FUNC #else #define _O_TEXT 0 #define _O_BINARY 0 #define lfs_setmode(L,file,m) ((void)L, (void)file, (void)m, 0) #define STAT_STRUCT struct stat #define STAT_FUNC stat #define LSTAT_FUNC lstat #endif /* ** Utility functions */ static int pusherror(lua_State *L, const char *info) { lua_pushnil(L); if (info==NULL) lua_pushstring(L, strerror(errno)); else lua_pushfstring(L, "%s: %s", info, strerror(errno)); lua_pushinteger(L, errno); return 3; } static int pushresult(lua_State *L, int i, const char *info) { if (i==-1) return pusherror(L, info); lua_pushinteger(L, i); return 1; } /* ** This function changes the working (current) directory */ static int change_dir (lua_State *L) { const char *path = luaL_checkstring(L, 1); if (chdir(path)) { lua_pushnil (L); lua_pushfstring (L,"Unable to change working directory to '%s'\n%s\n", path, chdir_error); return 2; } else { lua_pushboolean (L, 1); return 1; } } /* ** This function returns the current directory ** If unable to get the current directory, it returns nil ** and a string describing the error */ static int get_dir (lua_State *L) { char *path; /* Passing (NULL, 0) is not guaranteed to work. Use a temp buffer and size instead. */ char buf[LFS_MAXPATHLEN]; if ((path = getcwd(buf, LFS_MAXPATHLEN)) == NULL) { lua_pushnil(L); lua_pushstring(L, getcwd_error); return 2; } else { lua_pushstring(L, path); return 1; } } /* ** Check if the given element on the stack is a file and returns it. */ static FILE *check_file (lua_State *L, int idx, const char *funcname) { FILE **fh = (FILE **)luaL_checkudata (L, idx, "FILE*"); if (fh == NULL) { luaL_error (L, "%s: not a file", funcname); return 0; } else if (*fh == NULL) { luaL_error (L, "%s: closed file", funcname); return 0; } else return *fh; } /* ** */ static int _file_lock (lua_State *L, FILE *fh, const char *mode, const long start, long len, const char *funcname) { int code; #ifdef _WIN32 /* lkmode valid values are: LK_LOCK Locks the specified bytes. If the bytes cannot be locked, the program immediately tries again after 1 second. If, after 10 attempts, the bytes cannot be locked, the constant returns an error. LK_NBLCK Locks the specified bytes. If the bytes cannot be locked, the constant returns an error. LK_NBRLCK Same as _LK_NBLCK. LK_RLCK Same as _LK_LOCK. LK_UNLCK Unlocks the specified bytes, which must have been previously locked. Regions should be locked only briefly and should be unlocked before closing a file or exiting the program. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclib/html/_crt__locking.asp */ int lkmode; switch (*mode) { case 'r': lkmode = LK_NBLCK; break; case 'w': lkmode = LK_NBLCK; break; case 'u': lkmode = LK_UNLCK; break; default : return luaL_error (L, "%s: invalid mode", funcname); } if (!len) { fseek (fh, 0L, SEEK_END); len = ftell (fh); } fseek (fh, start, SEEK_SET); #ifdef __BORLANDC__ code = locking (fileno(fh), lkmode, len); #else code = _locking (fileno(fh), lkmode, len); #endif #else struct flock f; switch (*mode) { case 'w': f.l_type = F_WRLCK; break; case 'r': f.l_type = F_RDLCK; break; case 'u': f.l_type = F_UNLCK; break; default : return luaL_error (L, "%s: invalid mode", funcname); } f.l_whence = SEEK_SET; f.l_start = (off_t)start; f.l_len = (off_t)len; code = fcntl (fileno(fh), F_SETLK, &f); #endif return (code != -1); } #ifdef _WIN32 typedef struct lfs_Lock { HANDLE fd; } lfs_Lock; static int lfs_lock_dir(lua_State *L) { size_t pathl; HANDLE fd; lfs_Lock *lock; char *ln; const char *lockfile = "/lockfile.lfs"; const char *path = luaL_checklstring(L, 1, &pathl); ln = (char*)malloc(pathl + strlen(lockfile) + 1); if(!ln) { lua_pushnil(L); lua_pushstring(L, strerror(errno)); return 2; } strcpy(ln, path); strcat(ln, lockfile); if((fd = CreateFile(ln, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE, NULL)) == INVALID_HANDLE_VALUE) { int en = GetLastError(); free(ln); lua_pushnil(L); if(en == ERROR_FILE_EXISTS || en == ERROR_SHARING_VIOLATION) lua_pushstring(L, "File exists"); else lua_pushstring(L, strerror(en)); return 2; } free(ln); lock = (lfs_Lock*)lua_newuserdata(L, sizeof(lfs_Lock)); lock->fd = fd; luaL_getmetatable (L, LOCK_METATABLE); lua_setmetatable (L, -2); return 1; } static int lfs_unlock_dir(lua_State *L) { lfs_Lock *lock = luaL_checkudata(L, 1, LOCK_METATABLE); if(lock->fd != INVALID_HANDLE_VALUE) { CloseHandle(lock->fd); lock->fd=INVALID_HANDLE_VALUE; } return 0; } #else typedef struct lfs_Lock { char *ln; } lfs_Lock; static int lfs_lock_dir(lua_State *L) { lfs_Lock *lock; size_t pathl; char *ln; const char *lockfile = "/lockfile.lfs"; const char *path = luaL_checklstring(L, 1, &pathl); lock = (lfs_Lock*)lua_newuserdata(L, sizeof(lfs_Lock)); ln = (char*)malloc(pathl + strlen(lockfile) + 1); if(!ln) { lua_pushnil(L); lua_pushstring(L, strerror(errno)); return 2; } strcpy(ln, path); strcat(ln, lockfile); if(symlink("lock", ln) == -1) { free(ln); lua_pushnil(L); lua_pushstring(L, strerror(errno)); return 2; } lock->ln = ln; luaL_getmetatable (L, LOCK_METATABLE); lua_setmetatable (L, -2); return 1; } static int lfs_unlock_dir(lua_State *L) { lfs_Lock *lock = luaL_checkudata(L, 1, LOCK_METATABLE); if(lock->ln) { unlink(lock->ln); free(lock->ln); lock->ln = NULL; } return 0; } #endif static int lfs_g_setmode (lua_State *L, FILE *f, int arg) { static const int mode[] = {_O_BINARY, _O_TEXT}; static const char *const modenames[] = {"binary", "text", NULL}; int op = luaL_checkoption(L, arg, NULL, modenames); int res = lfs_setmode(L, f, mode[op]); if (res != -1) { int i; lua_pushboolean(L, 1); for (i = 0; modenames[i] != NULL; i++) { if (mode[i] == res) { lua_pushstring(L, modenames[i]); goto exit; } } lua_pushnil(L); exit: return 2; } else { int en = errno; lua_pushnil(L); lua_pushfstring(L, "%s", strerror(en)); lua_pushinteger(L, en); return 3; } } static int lfs_f_setmode(lua_State *L) { return lfs_g_setmode(L, check_file(L, 1, "setmode"), 2); } /* ** Locks a file. ** @param #1 File handle. ** @param #2 String with lock mode ('w'rite, 'r'ead). ** @param #3 Number with start position (optional). ** @param #4 Number with length (optional). */ static int file_lock (lua_State *L) { FILE *fh = check_file (L, 1, "lock"); const char *mode = luaL_checkstring (L, 2); const long start = (long)luaL_optinteger(L, 3, 0); long len = (long)luaL_optinteger(L, 4, 0); if (_file_lock (L, fh, mode, start, len, "lock")) { lua_pushboolean (L, 1); return 1; } else { lua_pushnil (L); lua_pushfstring (L, "%s", strerror(errno)); return 2; } } /* ** Unlocks a file. ** @param #1 File handle. ** @param #2 Number with start position (optional). ** @param #3 Number with length (optional). */ static int file_unlock (lua_State *L) { FILE *fh = check_file (L, 1, "unlock"); const long start = (long)luaL_optinteger(L, 2, 0); long len = (long)luaL_optinteger(L, 3, 0); if (_file_lock (L, fh, "u", start, len, "unlock")) { lua_pushboolean (L, 1); return 1; } else { lua_pushnil (L); lua_pushfstring (L, "%s", strerror(errno)); return 2; } } /* ** Creates a link. ** @param #1 Object to link to. ** @param #2 Name of link. ** @param #3 True if link is symbolic (optional). */ static int make_link(lua_State *L) { #ifndef _WIN32 const char *oldpath = luaL_checkstring(L, 1); const char *newpath = luaL_checkstring(L, 2); return pushresult(L, (lua_toboolean(L,3) ? symlink : link)(oldpath, newpath), NULL); #else return pusherror(L, "make_link is not supported on Windows"); #endif } /* ** Creates a directory. ** @param #1 Directory path. */ static int make_dir (lua_State *L) { const char *path = luaL_checkstring (L, 1); int fail; #ifdef _WIN32 fail = _mkdir (path); #else fail = mkdir (path, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH ); #endif if (fail) { lua_pushnil (L); lua_pushfstring (L, "%s", strerror(errno)); return 2; } lua_pushboolean (L, 1); return 1; } /* ** Removes a directory. ** @param #1 Directory path. */ static int remove_dir (lua_State *L) { const char *path = luaL_checkstring (L, 1); int fail; fail = rmdir (path); if (fail) { lua_pushnil (L); lua_pushfstring (L, "%s", strerror(errno)); return 2; } lua_pushboolean (L, 1); return 1; } /* ** Directory iterator */ static int dir_iter (lua_State *L) { #ifdef _WIN32 struct _finddata_t c_file; #else struct dirent *entry; #endif dir_data *d = (dir_data *)luaL_checkudata (L, 1, DIR_METATABLE); luaL_argcheck (L, d->closed == 0, 1, "closed directory"); #ifdef _WIN32 if (d->hFile == 0L) { /* first entry */ if ((d->hFile = _findfirst (d->pattern, &c_file)) == -1L) { lua_pushnil (L); lua_pushstring (L, strerror (errno)); d->closed = 1; return 2; } else { lua_pushstring (L, c_file.name); return 1; } } else { /* next entry */ if (_findnext (d->hFile, &c_file) == -1L) { /* no more entries => close directory */ _findclose (d->hFile); d->closed = 1; return 0; } else { lua_pushstring (L, c_file.name); return 1; } } #else if ((entry = readdir (d->dir)) != NULL) { lua_pushstring (L, entry->d_name); return 1; } else { /* no more entries => close directory */ closedir (d->dir); d->closed = 1; return 0; } #endif } /* ** Closes directory iterators */ static int dir_close (lua_State *L) { dir_data *d = (dir_data *)lua_touserdata (L, 1); #ifdef _WIN32 if (!d->closed && d->hFile) { _findclose (d->hFile); } #else if (!d->closed && d->dir) { closedir (d->dir); } #endif d->closed = 1; return 0; } /* ** Factory of directory iterators */ static int dir_iter_factory (lua_State *L) { const char *path = luaL_checkstring (L, 1); dir_data *d; lua_pushcfunction (L, dir_iter); d = (dir_data *) lua_newuserdata (L, sizeof(dir_data)); luaL_getmetatable (L, DIR_METATABLE); lua_setmetatable (L, -2); d->closed = 0; #ifdef _WIN32 d->hFile = 0L; if (strlen(path) > MAX_PATH-2) luaL_error (L, "path too long: %s", path); else sprintf (d->pattern, "%s/*", path); #else d->dir = opendir (path); if (d->dir == NULL) luaL_error (L, "cannot open %s: %s", path, strerror (errno)); #endif return 2; } /* ** Creates directory metatable. */ static int dir_create_meta (lua_State *L) { luaL_newmetatable (L, DIR_METATABLE); /* Method table */ lua_newtable(L); lua_pushcfunction (L, dir_iter); lua_setfield(L, -2, "next"); lua_pushcfunction (L, dir_close); lua_setfield(L, -2, "close"); /* Metamethods */ lua_setfield(L, -2, "__index"); lua_pushcfunction (L, dir_close); lua_setfield (L, -2, "__gc"); return 1; } /* ** Creates lock metatable. */ static int lock_create_meta (lua_State *L) { luaL_newmetatable (L, LOCK_METATABLE); /* Method table */ lua_newtable(L); lua_pushcfunction(L, lfs_unlock_dir); lua_setfield(L, -2, "free"); /* Metamethods */ lua_setfield(L, -2, "__index"); lua_pushcfunction(L, lfs_unlock_dir); lua_setfield(L, -2, "__gc"); return 1; } #ifdef _WIN32 #ifndef S_ISDIR #define S_ISDIR(mode) (mode&_S_IFDIR) #endif #ifndef S_ISREG #define S_ISREG(mode) (mode&_S_IFREG) #endif #ifndef S_ISLNK #define S_ISLNK(mode) (0) #endif #ifndef S_ISSOCK #define S_ISSOCK(mode) (0) #endif #ifndef S_ISFIFO #define S_ISFIFO(mode) (0) #endif #ifndef S_ISCHR #define S_ISCHR(mode) (mode&_S_IFCHR) #endif #ifndef S_ISBLK #define S_ISBLK(mode) (0) #endif #endif /* ** Convert the inode protection mode to a string. */ #ifdef _WIN32 static const char *mode2string (unsigned short mode) { #else static const char *mode2string (mode_t mode) { #endif if ( S_ISREG(mode) ) return "file"; else if ( S_ISDIR(mode) ) return "directory"; else if ( S_ISLNK(mode) ) return "link"; else if ( S_ISSOCK(mode) ) return "socket"; else if ( S_ISFIFO(mode) ) return "named pipe"; else if ( S_ISCHR(mode) ) return "char device"; else if ( S_ISBLK(mode) ) return "block device"; else return "other"; } /* ** Set access time and modification values for file */ static int file_utime (lua_State *L) { const char *file = luaL_checkstring (L, 1); struct utimbuf utb, *buf; if (lua_gettop (L) == 1) /* set to current date/time */ buf = NULL; else { utb.actime = (time_t)luaL_optnumber (L, 2, 0); utb.modtime = (time_t)luaL_optnumber (L, 3, utb.actime); buf = &utb; } if (utime (file, buf)) { lua_pushnil (L); lua_pushfstring (L, "%s", strerror (errno)); return 2; } lua_pushboolean (L, 1); return 1; } /* inode protection mode */ static void push_st_mode (lua_State *L, STAT_STRUCT *info) { lua_pushstring (L, mode2string (info->st_mode)); } /* device inode resides on */ static void push_st_dev (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_dev); } /* inode's number */ static void push_st_ino (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_ino); } /* number of hard links to the file */ static void push_st_nlink (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_nlink); } /* user-id of owner */ static void push_st_uid (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_uid); } /* group-id of owner */ static void push_st_gid (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_gid); } /* device type, for special file inode */ static void push_st_rdev (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_rdev); } /* time of last access */ static void push_st_atime (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, info->st_atime); } /* time of last data modification */ static void push_st_mtime (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, info->st_mtime); } /* time of last file status change */ static void push_st_ctime (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, info->st_ctime); } /* file size, in bytes */ static void push_st_size (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_size); } #ifndef _WIN32 /* blocks allocated for file */ static void push_st_blocks (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_blocks); } /* optimal file system I/O blocksize */ static void push_st_blksize (lua_State *L, STAT_STRUCT *info) { lua_pushnumber (L, (lua_Number)info->st_blksize); } #endif /* ** Convert the inode protection mode to a permission list. */ #ifdef _WIN32 static const char *perm2string (unsigned short mode) { static char perms[10] = "---------\0"; int i; for (i=0;i<9;i++) perms[i]='-'; if (mode & _S_IREAD) { perms[0] = 'r'; perms[3] = 'r'; perms[6] = 'r'; } if (mode & _S_IWRITE) { perms[1] = 'w'; perms[4] = 'w'; perms[7] = 'w'; } if (mode & _S_IEXEC) { perms[2] = 'x'; perms[5] = 'x'; perms[8] = 'x'; } return perms; } #else static const char *perm2string (mode_t mode) { static char perms[10] = "---------\0"; int i; for (i=0;i<9;i++) perms[i]='-'; if (mode & S_IRUSR) perms[0] = 'r'; if (mode & S_IWUSR) perms[1] = 'w'; if (mode & S_IXUSR) perms[2] = 'x'; if (mode & S_IRGRP) perms[3] = 'r'; if (mode & S_IWGRP) perms[4] = 'w'; if (mode & S_IXGRP) perms[5] = 'x'; if (mode & S_IROTH) perms[6] = 'r'; if (mode & S_IWOTH) perms[7] = 'w'; if (mode & S_IXOTH) perms[8] = 'x'; return perms; } #endif /* permssions string */ static void push_st_perm (lua_State *L, STAT_STRUCT *info) { lua_pushstring (L, perm2string (info->st_mode)); } typedef void (*_push_function) (lua_State *L, STAT_STRUCT *info); struct _stat_members { const char *name; _push_function push; }; struct _stat_members members[] = { { "mode", push_st_mode }, { "dev", push_st_dev }, { "ino", push_st_ino }, { "nlink", push_st_nlink }, { "uid", push_st_uid }, { "gid", push_st_gid }, { "rdev", push_st_rdev }, { "access", push_st_atime }, { "modification", push_st_mtime }, { "change", push_st_ctime }, { "size", push_st_size }, { "permissions", push_st_perm }, #ifndef _WIN32 { "blocks", push_st_blocks }, { "blksize", push_st_blksize }, #endif { NULL, NULL } }; /* ** Get file or symbolic link information */ static int _file_info_ (lua_State *L, int (*st)(const char*, STAT_STRUCT*)) { STAT_STRUCT info; const char *file = luaL_checkstring (L, 1); int i; if (st(file, &info)) { lua_pushnil (L); lua_pushfstring (L, "cannot obtain information from file `%s'", file); return 2; } if (lua_isstring (L, 2)) { const char *member = lua_tostring (L, 2); for (i = 0; members[i].name; i++) { if (strcmp(members[i].name, member) == 0) { /* push member value and return */ members[i].push (L, &info); return 1; } } /* member not found */ return luaL_error(L, "invalid attribute name"); } /* creates a table if none is given */ if (!lua_istable (L, 2)) { lua_newtable (L); } /* stores all members in table on top of the stack */ for (i = 0; members[i].name; i++) { lua_pushstring (L, members[i].name); members[i].push (L, &info); lua_rawset (L, -3); } return 1; } /* ** Get file information using stat. */ static int file_info (lua_State *L) { return _file_info_ (L, STAT_FUNC); } /* ** Get symbolic link information using lstat. */ static int link_info (lua_State *L) { return _file_info_ (L, LSTAT_FUNC); } /* ** Assumes the table is on top of the stack. */ static void set_info (lua_State *L) { lua_pushliteral (L, "_COPYRIGHT"); lua_pushliteral (L, "Copyright (C) 2003-2012 Kepler Project"); lua_settable (L, -3); lua_pushliteral (L, "_DESCRIPTION"); lua_pushliteral (L, "LuaFileSystem is a Lua library developed to complement the set of functions related to file systems offered by the standard Lua distribution"); lua_settable (L, -3); lua_pushliteral (L, "_VERSION"); lua_pushliteral (L, "LuaFileSystem "LFS_VERSION); lua_settable (L, -3); } static const struct luaL_Reg fslib[] = { {"attributes", file_info}, {"chdir", change_dir}, {"currentdir", get_dir}, {"dir", dir_iter_factory}, {"link", make_link}, {"lock", file_lock}, {"mkdir", make_dir}, {"rmdir", remove_dir}, {"symlinkattributes", link_info}, {"setmode", lfs_f_setmode}, {"touch", file_utime}, {"unlock", file_unlock}, {"lock_dir", lfs_lock_dir}, {NULL, NULL}, }; int luaopen_lfs (lua_State *L) { dir_create_meta (L); lock_create_meta (L); luaL_newlib (L, fslib); lua_pushvalue(L, -1); lua_setglobal(L, LFS_LIBNAME); set_info (L); return 1; } ================================================ FILE: src/lfs.h ================================================ /* ** LuaFileSystem ** Copyright Kepler Project 2003 (http://www.keplerproject.org/luafilesystem) ** ** $Id: lfs.h,v 1.5 2008/02/19 20:08:23 mascarenhas Exp $ */ /* Define 'chdir' for systems that do not implement it */ #ifdef NO_CHDIR #define chdir(p) (-1) #define chdir_error "Function 'chdir' not provided by system" #else #define chdir_error strerror(errno) #endif extern int luaopen_lfs (lua_State *L); ================================================ FILE: src/lua_lpack.c ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include #include #include "uv.h" #include "luaw_common.h" #include "lua_lpack.h" static void be16(const char* in, char * out) { if (is_bigendian()) { out[0] = in[0]; out[1] = in[1]; } else { out[0] = in[1]; out[1] = in[0]; } } static void be32(const char* in, char* out) { if (is_bigendian()) { out[0] = in[0]; out[1] = in[1]; out[2] = in[2]; out[3] = in[3]; } else { out[0] = in[3]; out[3] = in[0]; out[1] = in[2]; out[2] = in[1]; } } static void be64(const char* in, char* out) { if (is_bigendian()) { out[0] = in[0]; out[1] = in[1]; out[2] = in[2]; out[3] = in[3]; out[4] = in[4]; out[5] = in[5]; out[6] = in[6]; out[7] = in[7]; } else { out[0] = in[7]; out[7] = in[0]; out[1] = in[6]; out[6] = in[1]; out[2] = in[5]; out[5] = in[2]; out[3] = in[4]; out[4] = in[3]; } } static int number_to_lua(lua_State* L, int read_len, double value) { lua_pushinteger(L, read_len); lua_pushnumber(L, value); return 2; } LUA_LIB_METHOD int read_number(lua_State* L) { int num_type = luaL_checkinteger(L, 1); size_t len; const char* buff = luaL_checklstring(L, 2, &len); size_t offset = luaL_checkinteger(L, 3); int remaining = len - offset; if (remaining < 0) { luaL_error(L, "Buffer underflow while reading number"); } buff = buff + offset; size_t sz; switch(num_type) { case TYPE_MARKER: case UINT_8: case STRING: case DICT_ENTRY: if (remaining < 1) return number_to_lua(L, 0, 0); uint8_t u = buff[0]; return number_to_lua(L, 1, u); case UINT_16: case BIG_STRING: case BIG_DICT_ENTRY: if (remaining < 2) return number_to_lua(L, 0, 0); uint16_t u16; be16(buff, (char *)&u16); return number_to_lua(L, 2, u16); case UINT_32: case HUGE_STRING: if (remaining < 4) return number_to_lua(L, 0, 0); uint32_t u32; be32(buff, (char *)&u32); return number_to_lua(L, 4, u32); case INT_8: if (remaining < 1) return number_to_lua(L, 0, 0); int8_t i = buff[0]; return number_to_lua(L, 1, i); case INT_16: if (remaining < 2) return number_to_lua(L, 0, 0); int16_t i16; be16(buff, (char *)&i16); return number_to_lua(L, 2, i16); case INT_32: if (remaining < 4) return number_to_lua(L, 0, 0); int32_t i32; be32(buff, (char *)&i32); return number_to_lua(L, 4, i32); case INT_64: if (remaining < 8) return number_to_lua(L, 0, 0); int64_t i64; be64(buff, (char *)&i64); return number_to_lua(L, 8, i64); case FLOAT: sz = sizeof(float); if (remaining < sz) return number_to_lua(L, 0, 0); float f; be32(buff, (char *)&f); return number_to_lua(L, sz, f); case DOUBLE: sz = sizeof(double); if (remaining < sz) return number_to_lua(L, 0, 0); double d; be64(buff, (char *)&d); return number_to_lua(L, sz, d); default: return luaL_error(L, "Invalid marker %d specified", num_type); } } static int string_to_lua(lua_State* L, const char* buff, size_t len) { lua_pushinteger(L, len); lua_pushlstring(L, buff, len); return 2; } LUA_LIB_METHOD int read_string(lua_State* L) { int desired = luaL_checkinteger(L, 1); size_t len; const char* buff = luaL_checklstring(L, 2, &len); size_t offset = luaL_checkinteger(L, 3); int remaining = len - offset; if (remaining < 0) { luaL_error(L, "Buffer underflow while reading string"); } buff = buff + offset; if (remaining >= desired) { return string_to_lua(L, buff, desired); } else { return string_to_lua(L, buff, remaining); } } static double fetch_next_as_integer(lua_State* L, int* idx, const char* s) { lua_rawgeti(L, 1, *idx); if (!lua_isnumber(L, -1)) { const char* dbg_val = lua_tostring(L, -1); luaL_error(L, "Invalid value %s found where %s was expected",dbg_val, s); } int i = lua_tointeger(L, -1); *idx = *idx + 1; lua_pop(L, 1); return i; } static double fetch_next_as_double(lua_State* L, int* idx, const char* s) { lua_rawgeti(L, 1, *idx); if (!lua_tonumber(L, -1)) { const char* dbg_val = lua_tostring(L, -1); luaL_error(L, "Invalid value %s found where %s was expected", dbg_val, s); } double d = lua_tonumber(L, -1); *idx = *idx + 1; lua_pop(L, 1); return d; } static const char* fetch_next_as_string(lua_State* L, size_t *len, int* idx, const char* s) { lua_rawgeti(L, 1, *idx); const char* val = lua_tolstring(L, -1, len); if (val == NULL) { luaL_error(L, "Invalid value found where %s was expected", s); } *idx = *idx + 1; lua_pop(L, 1); return val; } #define CHECK_ROOM(L, pos, need, size) \ if ((pos+need) > size) { \ return luaL_error(L, "Run out of buffer in serialize_write_Q, wrong expected size supplied"); \ } LUA_LIB_METHOD int serialize_write_Q(lua_State* L) { if (lua_istable(L, 1) == 0) { return luaL_error(L, "Invalid WriteQ, not a table"); } size_t len = lua_tointeger(L, 2); if (len == 0) { return luaL_error(L, "Invalid write buffer length specified"); } const int buffsize = len + 64; char* buff = (char*) malloc(buffsize); if (buff == NULL) { return luaL_error(L, "Could not allocate memory for serialize_write_Q"); } int pos = 0; int ival; double dval; uint16_t u16; uint32_t u32; int16_t i16; int32_t i32; int64_t i64; const char *sval; int qLen = lua_rawlen(L, 1); int idx = 1; int datasize = 0; while (idx <= qLen) { // write marker int marker = fetch_next_as_integer(L, &idx, "marker"); CHECK_ROOM(L, pos, 1, buffsize) buff[pos] = (char)marker; pos++; switch(marker) { case MAP_START: case ARRAY_START: case DICT_START: case RECORD_END: case NIL: case BOOL_TRUE: case BOOL_FALSE: break; //No value, single byte markers case UINT_8: case DICT_ENTRY: datasize = sizeof(uint8_t); CHECK_ROOM(L, pos, datasize, buffsize) ival = fetch_next_as_integer(L, &idx, "UINT_8"); buff[pos] = (uint8_t)ival; pos += datasize; break; case UINT_16: case BIG_DICT_ENTRY: datasize = sizeof(uint16_t); CHECK_ROOM(L, pos, datasize, buffsize) ival = fetch_next_as_integer(L, &idx, "UINT_16 or BIG_DICT_ENTRY"); u16 = (uint16_t)ival; be16((char *)&u16, buff+pos); pos += datasize; break; case UINT_32: datasize = sizeof(uint32_t); ival = fetch_next_as_integer(L, &idx, "UINT_32"); u32 = (uint32_t)ival; be32((char *)&u32, buff+pos); pos += datasize; break; case INT_8: datasize = sizeof(int8_t); CHECK_ROOM(L, pos, datasize, buffsize) ival = fetch_next_as_integer(L, &idx, "INT_8"); buff[pos] = (int8_t)ival; pos += datasize; break; case INT_16: datasize = sizeof(int16_t); CHECK_ROOM(L, pos, datasize, buffsize) ival = fetch_next_as_integer(L, &idx, "INT_16"); i16 = (int16_t)ival; be16((char *)&i16, buff+pos); pos += datasize; break; case INT_32: datasize = sizeof(int32_t); CHECK_ROOM(L, pos, datasize, buffsize) ival = fetch_next_as_integer(L, &idx, "INT_32"); i32 = (int32_t)ival; be32((char *)&i32, buff+pos); pos += datasize; break; case INT_64: datasize = sizeof(int64_t); CHECK_ROOM(L, pos, datasize, buffsize) dval = fetch_next_as_double(L, &idx, "INT_64"); i64 = (int64_t)dval; be64((char *)&i64, buff+pos); pos += datasize; break; case FLOAT: datasize = sizeof(float); CHECK_ROOM(L, pos, datasize, buffsize) dval = fetch_next_as_double(L, &idx, "FLOAT"); float f = (float)dval; be32((char *)&f, buff+pos); pos += datasize; break; case DOUBLE: datasize = sizeof(double); CHECK_ROOM(L, pos, datasize, buffsize) dval = fetch_next_as_double(L, &idx, "DOUBLE"); be64((char *)&dval, buff+pos); pos += datasize; break; case STRING: datasize = sizeof(uint8_t); sval = fetch_next_as_string(L, &len, &idx, "STRING"); CHECK_ROOM(L, pos, datasize+len, buffsize) buff[pos] = (uint8_t)len; pos += datasize; memcpy(buff+pos, sval, len); pos += len; break; case BIG_STRING: datasize = sizeof(uint16_t); sval = fetch_next_as_string(L, &len, &idx, "BIG_STRING"); CHECK_ROOM(L, pos, datasize+len, buffsize) u16 = (uint16_t)len; be16((char *)&u16, buff+pos); pos += datasize; memcpy(buff+pos, sval, len); pos += len; break; case HUGE_STRING: datasize = sizeof(uint32_t); sval = fetch_next_as_string(L, &len, &idx, "HUGE_STRING"); CHECK_ROOM(L, pos, datasize+len, buffsize) u32 = (uint32_t)len; be32((char *)&u32, buff+pos); pos += datasize; memcpy(buff+pos, sval, len); pos += len; break; default: return luaL_error(L, "Invalid marker %d encountered", marker); } } lua_pushlstring(L, buff, pos); free(buff); return 1; } /* Type enum definition in Lua: lpack.INT_8 = {"INT_8", 14, 1, -128, 127} */ static void define_lpack_enum(lua_State* L, const char* name, type_tag tval, size_t size, double min, double max) { lua_pushstring(L, name); lua_createtable(L, 5, 0); lua_pushstring(L, name); lua_rawseti(L, -2, 1); lua_pushinteger(L, tval); lua_rawseti(L, -2, 2); lua_pushinteger(L, size); lua_rawseti(L, -2, 3); lua_pushnumber(L, min); lua_rawseti(L, -2, 4); lua_pushnumber(L, max); lua_rawseti(L, -2, 5); lua_rawset(L, -3); } #define LPACK_ENUM_DEF(L, e, size, min, max) define_lpack_enum(L, #e, e, size, min, max) static void define_lapck_types(lua_State* L) { LPACK_ENUM_DEF(L, TYPE_MARKER, 0, -1, -1); LPACK_ENUM_DEF(L, MAP_START, sizeof(int8_t), MAP_START, MAP_START); LPACK_ENUM_DEF(L, ARRAY_START, sizeof(int8_t), ARRAY_START, ARRAY_START); LPACK_ENUM_DEF(L, DICT_START, sizeof(int8_t), DICT_START, DICT_START); LPACK_ENUM_DEF(L, RECORD_END, sizeof(int8_t), RECORD_END, RECORD_END); LPACK_ENUM_DEF(L, NIL, sizeof(int8_t), NIL, NIL); LPACK_ENUM_DEF(L, BOOL_TRUE, sizeof(int8_t), BOOL_TRUE, BOOL_TRUE); LPACK_ENUM_DEF(L, BOOL_FALSE, sizeof(int8_t), BOOL_FALSE, BOOL_FALSE); LPACK_ENUM_DEF(L, UINT_8, sizeof(uint8_t), 0, UINT8_MAX); LPACK_ENUM_DEF(L, DICT_ENTRY, sizeof(uint8_t), 0, UINT8_MAX); LPACK_ENUM_DEF(L, UINT_16, sizeof(uint16_t), 0, UINT16_MAX); LPACK_ENUM_DEF(L, BIG_DICT_ENTRY, sizeof(uint16_t), 0, UINT16_MAX); LPACK_ENUM_DEF(L, UINT_32, sizeof(uint32_t), 0, UINT32_MAX); LPACK_ENUM_DEF(L, INT_8, sizeof(int8_t), INT8_MIN, INT8_MAX); LPACK_ENUM_DEF(L, INT_16, sizeof(int16_t), INT16_MIN, INT16_MAX); LPACK_ENUM_DEF(L, INT_32, sizeof(int32_t), INT32_MIN, INT32_MAX); LPACK_ENUM_DEF(L, INT_64, sizeof(int64_t), INT64_MIN, INT64_MAX); LPACK_ENUM_DEF(L, FLOAT, sizeof(float), -FLT_MAX, FLT_MAX); LPACK_ENUM_DEF(L, DOUBLE, sizeof(double), -DBL_MAX, DBL_MAX); LPACK_ENUM_DEF(L, STRING, sizeof(uint8_t), 0, UINT8_MAX); LPACK_ENUM_DEF(L, BIG_STRING, sizeof(uint16_t), 0, UINT16_MAX); LPACK_ENUM_DEF(L, HUGE_STRING, sizeof(uint32_t), 0, UINT32_MAX); } LUA_LIB_METHOD static int new_lpack_parser(lua_State* L) { lua_createtable(L, 0, 16); luaL_setmetatable(L, LUA_LPACK_META_TABLE); return 1; } LUA_LIB_METHOD static int create_dict(lua_State* L) { int narr = luaL_checkinteger(L, 1); int nrec = luaL_checkinteger(L, 2); lua_createtable(L, narr, nrec); return 1; } LUA_LIB_METHOD static int to_hex(lua_State* L) { int num = luaL_checkinteger(L, -1); if (num > 65536) { raise_lua_error(L, "toHex called with input %d, which is larger than acceptable limit", num); } char hex[5]; sprintf(hex, "%x", num); lua_pushstring(L, hex); return 1; } static const struct luaL_Reg luaw_lpack_methods[] = { {"read_number", read_number}, {"read_string", read_string}, {"serialize_write_Q", serialize_write_Q}, {NULL, NULL} /* sentinel */ }; static const struct luaL_Reg luaw_lpack_lib[] = { {"newLPackParser", new_lpack_parser}, {"createDict", create_dict}, {"toHex", to_hex}, {NULL, NULL} /* sentinel */ }; void luaw_init_lpack_lib (lua_State *L) { make_metatable(L, LUA_LPACK_META_TABLE, luaw_lpack_methods); define_lapck_types(L); luaL_newlib(L, luaw_lpack_lib); lua_setglobal(L, "luaw_lpack_lib"); } ================================================ FILE: src/lua_lpack.h ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #define LUA_LPACK_META_TABLE "_luaw_lpack_MT_" static const int32_t _i = 1; #define is_bigendian() ((*(char *)&_i) == 0) typedef enum { /* meta type for all marker tags, should never itself occur in read stream */ TYPE_MARKER = 0, /* single byte marker tags */ MAP_START, ARRAY_START, DICT_START, RECORD_END, /* single byte value markers */ NIL, BOOL_TRUE, BOOL_FALSE, /* fix width types */ UINT_8, DICT_ENTRY, UINT_16, BIG_DICT_ENTRY, UINT_32, INT_8, INT_16, INT_32, INT_64, FLOAT, DOUBLE, /* variable length types */ STRING, BIG_STRING, HUGE_STRING } type_tag; extern void luaw_init_lpack_lib (lua_State *L); ================================================ FILE: src/luaw_common.c ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include #include #include "uv.h" #include "luaw_common.h" #include "luaw_logging.h" #include "luaw_tcp.h" #include "luaw_http_parser.h" #include "luaw_timer.h" #include "lua_lpack.h" /* globals */ lua_State* l_global = NULL; //main global Lua state that spawns all other coroutines int resume_thread_fn_ref; //Resume thread lua function void resume_lua_thread(lua_State* L, int nargs, int nresults, int errHandler) { int tid = lua_tointeger(L, (0 - nargs)); if (tid) { int rc = lua_pcall(L, nargs, nresults, errHandler); if (rc != 0) { fprintf(stderr, "******** Error resuming Lua thread# %d: %s (%d) *********\n", tid, lua_tostring(L, -1), rc); } } } /* sets up error code and error message on the Lua stack to be returned by the original C function called from Lua. */ int error_to_lua(lua_State* L, const char* fmt, ...) { lua_settop(L, 0); //remove success status and params table from stack lua_pushboolean(L, 0); //set status to false in case of error va_list argp; va_start(argp, fmt); lua_pushvfstring(L, fmt, argp); va_end(argp); return 2; //two values status and err_mesg pushed onto stack. } int raise_lua_error (lua_State *L, const char *fmt, ...) { va_list argp; va_start(argp, fmt); lua_pushvfstring(L, fmt, argp); va_end(argp); return lua_error(L); } void close_if_active(uv_handle_t* handle, uv_close_cb close_cb) { if ((handle != NULL)&&(!uv_is_closing(handle))) { uv_close(handle, close_cb); } } /* Minimal LuaJIT compatibility layer adopted from https://github.com/keplerproject/lua-compat-5.2 */ #if !defined(LUA_VERSION_NUM) || LUA_VERSION_NUM == 501 void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { luaL_checkstack(L, nup+1, "too many upvalues"); for (; l->name != NULL; l++) { /* fill the table with given functions */ int i; lua_pushstring(L, l->name); for (i = 0; i < nup; i++) /* copy upvalues to the top */ lua_pushvalue(L, -(nup + 1)); lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ lua_settable(L, -(nup + 3)); /* table must be below the upvalues, the name and the closure */ } lua_pop(L, nup); /* remove upvalues */ } void luaL_setmetatable (lua_State *L, const char *tname) { luaL_checkstack(L, 1, "not enough stack slots"); luaL_getmetatable(L, tname); lua_setmetatable(L, -2); } #endif void make_metatable(lua_State *L, const char* mt_name, const luaL_Reg* mt_funcs) { luaL_newmetatable(L, mt_name); luaL_setfuncs(L, mt_funcs, 0); /* client metatable.__index = client metatable */ lua_pushstring(L, "__index"); lua_pushvalue(L,-2); lua_rawset(L, -3); } LUA_LIB_METHOD void luaw_init_libs (lua_State *L) { luaw_init_logging_lib(L); luaw_init_tcp_lib(L); luaw_init_http_lib(L); luaw_init_timer_lib(L); luaw_init_lpack_lib(L); } /********************************************************************* * This file contains parts of Lua 5.2's source code: * * Copyright (C) 1994-2013 Lua.org, PUC-Rio. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *********************************************************************/ ================================================ FILE: src/luaw_common.h ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #ifndef LUAW_COMMON_H #define LUAW_COMMON_H typedef enum { false = 0, true } bool; /* marker macros for documenting code */ #define LIBUV_CALLBACK #define LIBUV_API #define LUA_OBJ_METHOD #define LUA_LIB_METHOD #define HTTP_PARSER_CALLBACK #define Q(x) #x #define QUOTE(x) Q(x) #define INCR_REF_COUNT(s) if (s != NULL) s->ref_count++; #define DECR_REF_COUNT(s) if (s != NULL) s->ref_count--; #define GC_REF(s) \ if (s != NULL) { \ s->ref_count--; \ if (s->ref_count <= 0) free(s); \ } #define step fprintf(stdout, "At line# %d function(%s) in file %s\n", __LINE__, __FUNCTION__, __FILE__); #define debug_i(s) fprintf(stdout, #s "= %d at line# %d function(%s) in file %s\n", s, __LINE__, __FUNCTION__, __FILE__); #define debug_l(s) fprintf(stdout, #s "= %ld at line# %d function(%s) in file %s\n", s, __LINE__, __FUNCTION__, __FILE__); #define debug_s(s) fprintf(stdout, #s "= %s at line# %d function(%s) in file %s\n", s, __LINE__, __FUNCTION__, __FILE__); #define debug_sl(s, l) fprintf(stdout, #s "= %.*s at line# %d function(%s) in file %s\n", l, s, __LINE__, __FUNCTION__, __FILE__); #define debug_p(s) fprintf(stdout, #s "= %p at line# %d function(%s) in file %s\n", s, __LINE__, __FUNCTION__, __FILE__); #define debug_break(s) fprintf(stdout, "%s\n", s); /* global state */ extern lua_State* l_global; extern int resume_thread_fn_ref; extern int error_to_lua(lua_State* L, const char* fmt, ...); extern int raise_lua_error (lua_State *L, const char *fmt, ...); extern void make_metatable(lua_State *L, const char* mt_name, const luaL_Reg* mt_funcs); extern void luaw_init_libs(lua_State *L); extern void close_if_active(uv_handle_t* handle, uv_close_cb close_cb); extern void resume_lua_thread(lua_State* L, int nargs, int nresults, int errHandler); /* Minimal LuaJIT compatibility layer adopted from https://github.com/keplerproject/lua-compat-5.2 */ #if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 /* Lua 5.1 */ /* PUC-Rio Lua uses lconfig_h as include guard for luaconf.h, * LuaJIT uses luaconf_h. If you use PUC-Rio's include files * but LuaJIT's library, you will need to define the macro * COMPAT52_IS_LUAJIT yourself! */ #if !defined(COMPAT52_IS_LUAJIT) && defined(luaconf_h) #define COMPAT52_IS_LUAJIT #endif /* LuaJIT doesn't define these unofficial macros ... */ #if !defined(LUAI_INT32) #include #if INT_MAX-20 < 32760 #define LUAI_INT32 long #define LUAI_UINT32 unsigned long #elif INT_MAX > 2147483640L #define LUAI_INT32 int #define LUAI_UINT32 unsigned int #else #error "could not detect suitable lua_Unsigned datatype" #endif #endif #define lua_rawlen(L, i) lua_objlen(L, i) #endif /* Lua 5.1 */ #if !defined(LUA_VERSION_NUM) || LUA_VERSION_NUM == 501 /* Lua 5.0 *or* 5.1 */ #define LUA_OK 0 #define lua_pushglobaltable(L) \ lua_pushvalue(L, LUA_GLOBALSINDEX) #define luaL_newlib(L, l) \ (lua_newtable((L)),luaL_setfuncs((L), (l), 0)) void luaL_checkversion (lua_State *L); #endif /* Lua 5.0 *or* 5.1 */ void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup); void luaL_setmetatable (lua_State *L, const char *tname); #endif ================================================ FILE: src/luaw_http_parser.c ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include "uv.h" #include "luaw_common.h" #include "luaw_http_parser.h" #include "luaw_tcp.h" typedef enum { PARSE_HTTP_PARSER_IDX = 1, PARSE_HTTP_BUFF_IDX, PARSE_HTTP_REQ_IDX, PARSE_HTTP_LENGTH_IDX, PARSE_HTTP_CONN_IDX } parse_http_lua_stack_index; static int decode_hex_str(const char* str, int len) { char *read_ptr = (char *)str; char *write_ptr = (char *)str; int hex_ch; char ch; while (len) { ch = *read_ptr; if ((ch == '%')&&(len > 2)) { sscanf((read_ptr+1),"%2x", &hex_ch); ch = (char)hex_ch; read_ptr += 3; len -= 3; } else { read_ptr++; len--; } *write_ptr = ch; write_ptr++; } return ((const unsigned char*)write_ptr - (const unsigned char*)str); } static int handle_name_value_pair(lua_State* L, const char* name, int name_len, bool hex_name, const char* value, int value_len, bool hex_value) { if ((name != NULL)&&(name_len > 0)&&(value != NULL)&&(value_len > 0)) { if (hex_name) { name_len = decode_hex_str(name, name_len); } if (hex_value) { value_len = decode_hex_str(value, value_len); } lua_getfield(L, 1, "storeHttpParam"); if (lua_isfunction(L, -1) != 1) { raise_lua_error(L, "Missing lua function storeHttpParam()"); } lua_pushvalue(L, 3); lua_pushlstring(L, name, name_len); lua_pushlstring(L, value, value_len) ; lua_call(L, 3, 0); } else if((name_len == 0)&&(value_len > 0)) { error_to_lua(L, "400 Bad URL encoding: empty parameter name, non empty parameter value: %s ", value); return false; } return true; //param name without value is ok, e.g. &foo= } /* Lua call spec: success: params table, nil = http_lib:url_decode(url encoded string, params) success: status(false), error message = http_lib:url_decode(url encoded string, params) */ LUA_LIB_METHOD static int luaw_url_decode(lua_State *L) { if (!lua_istable(L, 1)) { return raise_lua_error(L, "Luaw HTTP lib table is missing"); } size_t length = 0; const char* data = lua_tolstring(L, 2, &length); char *read_ptr = (char *)data; char *name = NULL; char *value = NULL; bool hex_name = false, hex_value = false; int name_len = 0, value_len = 0; decoder_state ds = starting_name; while (length--) { char ch = *read_ptr; switch(ds) { case starting_name: if (!handle_name_value_pair(L, name, name_len, hex_name, value, value_len, hex_value)) return 2; //err_code, err_mesg name_len = 0; hex_name = false; value_len = 0; hex_value = false; switch(ch) { case '&': case '=': return error_to_lua(L, "400 Bad URL encoding: Error while expecting start of param name at: %s\n", read_ptr); case '%': hex_name = true; } ds = in_name; name = read_ptr; name_len = 1; break; case in_name: switch(ch) { case '&': return error_to_lua(L, "400 Bad URL encoding: Error while parsing param name at: %s\n", read_ptr); case '=': ds = starting_value; break; case '%': hex_name = true; default: name_len++; } break; case starting_value: switch(ch) { case '&': case '=': return error_to_lua(L, "400 Bad URL encoding: Error while expecting start of param value at: %s\n", read_ptr); case '%': hex_value = true; } ds = in_value; value = read_ptr; value_len = 1; break; case in_value: switch(ch) { case '&': ds = starting_name; break; case '=': return error_to_lua(L, "400 Bad URL encoding: Error while parsing param value at: %s\n", read_ptr); case '%': hex_value = true; default: value_len++; } break; } if (ch == '+') *read_ptr =' '; read_ptr++; } return handle_name_value_pair(L, name, name_len, hex_name, value, value_len, hex_value) ? 1 : 2; } static int new_lhttp_parser(lua_State *L, enum http_parser_type parser_type) { luaw_http_parser_t* lhttp_parser = lua_newuserdata(L, sizeof(luaw_http_parser_t)); if (lhttp_parser == NULL) { return raise_lua_error(L, "Failed to allocate memory for new http_parser."); } luaL_setmetatable(L, LUA_HTTP_PARSER_META_TABLE); http_parser_init(&lhttp_parser->parser, parser_type); lhttp_parser->parser.data = lhttp_parser; return 1; } LUA_LIB_METHOD static int luaw_new_http_request_parser(lua_State* L) { return new_lhttp_parser(L, HTTP_REQUEST); } LUA_LIB_METHOD static int luaw_new_http_response_parser(lua_State* L) { return new_lhttp_parser(L, HTTP_RESPONSE); } LUA_LIB_METHOD static int luaw_init_http_parser(lua_State* L) { luaw_http_parser_t* lhttp_parser = luaL_checkudata(L, 1, LUA_HTTP_PARSER_META_TABLE); http_parser* parser = &lhttp_parser->parser; http_parser_init(parser, parser->type); return 0; } static int handle_http_callback(http_parser *parser, http_parser_cb_type cb, const char* start, size_t len) { luaw_http_parser_t* lhttp_parser = (luaw_http_parser_t*) parser->data; lhttp_parser->http_cb = cb; lhttp_parser->start = (char*)start; lhttp_parser->len = len; http_parser_pause(parser, 1); return 0; } HTTP_PARSER_CALLBACK static int http_parser_on_message_begin(http_parser *parser) { return handle_http_callback(parser, http_cb_on_message_begin, NULL, 0); } HTTP_PARSER_CALLBACK static int http_parser_on_url(http_parser *parser, const char* start, size_t len) { return handle_http_callback(parser, http_cb_on_url, start, len); } HTTP_PARSER_CALLBACK static int http_parser_on_status(http_parser *parser, const char* start, size_t len) { return handle_http_callback(parser, http_cb_on_status, start, len); } HTTP_PARSER_CALLBACK static int http_parser_on_header_name(http_parser *parser, const char* start, size_t len) { return handle_http_callback(parser, http_cb_on_header_field, start, len); } HTTP_PARSER_CALLBACK static int http_parser_on_header_value(http_parser *parser, const char* start, size_t len) { return handle_http_callback(parser, http_cb_on_header_value, start, len); } HTTP_PARSER_CALLBACK static int http_parser_on_headers_complete(http_parser *parser) { return handle_http_callback(parser, http_cb_on_headers_complete, NULL, 0); } HTTP_PARSER_CALLBACK static int http_parser_on_body(http_parser *parser, const char* start, size_t len) { return handle_http_callback(parser, http_cb_on_body, start, len); } HTTP_PARSER_CALLBACK static int http_parser_on_message_complete(http_parser *parser) { return handle_http_callback(parser, http_cb_on_mesg_complete, NULL, 0); } static const http_parser_settings parser_settings = { .on_message_begin = http_parser_on_message_begin, .on_status = http_parser_on_status, .on_url = http_parser_on_url, .on_header_field = http_parser_on_header_name, .on_header_value = http_parser_on_header_value, .on_headers_complete = http_parser_on_headers_complete, .on_body = http_parser_on_body, .on_message_complete = http_parser_on_message_complete }; /* Lua call spec: * All failures: * false, error message = parser:parseHttp(str, offset) * * Successes: * * http_parser_on_headers_complete: * http_cb_type, new offset, http_should_keep_alive, major_version, minor_version, http method, http status code = parseHttp(conn) * * http_parser_on_message_complete: * http_cb_type, new offset, http_should_keep_alive = parser:parseHttp(str, offset) * * All other callbacks: * http_cb_type, new offset, parsed value = parser:parseHttp(conn, str, offset) * */ static int parse_http(lua_State *L) { lua_settop(L, 3); luaw_http_parser_t* lhttp_parser = luaL_checkudata(L, 1, LUA_HTTP_PARSER_META_TABLE); http_parser* parser = &lhttp_parser->parser; size_t len = 0; const char* buff = lua_tolstring(L, 2, &len); int offset = lua_tointeger(L, 3); if (offset > len) { lua_pushboolean(L, 0); lua_pushfstring(L, "Wrong offset into HTTP string: len=%d, offset=%d, content=%s\n", len, offset, buff); return 2; } /* every http_parser_execute() does not necessarily cause callback to be invoked, we need to know if it did call the callback */ lhttp_parser->http_cb = http_cb_none; const int nparsed = http_parser_execute(parser, &parser_settings, (buff+offset), (len-offset)); offset += nparsed; const int remaining = len - offset; if ((remaining > 0)&&(parser->http_errno != HPE_PAUSED)) { lua_pushboolean(L, 0); lua_pushfstring(L, "Error parsing HTTP fragment: errorCode=%d, total=%d, parsed=%d, content=%s\n", parser->http_errno, len, nparsed, buff); return 2; } lua_pushinteger(L, lhttp_parser->http_cb); lua_pushinteger(L, offset); int nresults = 3; switch(lhttp_parser->http_cb) { case http_cb_on_headers_complete: lua_pushboolean(L, http_should_keep_alive(parser)); lua_pushinteger(L, parser->http_major); lua_pushinteger(L, parser->http_minor); if (parser->type == HTTP_REQUEST) { lua_pushstring(L, http_methods[parser->method]); } else { lua_pushnil(L); } if (parser->type == HTTP_RESPONSE) { lua_pushinteger(L, parser->status_code); } else { lua_pushnil(L); } nresults = 7; break; case http_cb_none: case http_cb_on_mesg_complete: lua_pushboolean(L, http_should_keep_alive(parser)); break; default: lua_pushlstring(L, lhttp_parser->start, lhttp_parser->len); } /* un-pause parser */ http_parser_pause(parser, 0); return nresults; } const char* url_field_names[] = { "schema", "host", "port", "path", "queryString", "fragment", "userInfo" }; static void push_url_part(lua_State *L, const char* buff, struct http_parser_url* parsed_url, enum http_parser_url_fields url_field) { if ((parsed_url->field_set) & (1 << url_field)) { lua_pushlstring(L, (buff + parsed_url->field_data[url_field].off), parsed_url->field_data[url_field].len); lua_setfield(L, -2, url_field_names[url_field]); } } /* Lua call spec: * Success: url parts table = http_lib.parseURL(url_string, is_connect) * Failure: nil = http_lib.parseURL(url_string, is_connect) */ LUA_LIB_METHOD static int luaw_parse_url(lua_State *L) { size_t len = 0; const char* buff = luaL_checklstring(L, 1, &len); int is_connect = lua_toboolean(L, 2); struct http_parser_url* parsed_url = (struct http_parser_url*) malloc(sizeof(struct http_parser_url)); if (parsed_url == NULL) { return raise_lua_error(L, "Could not allocate memory for URL struct"); } int result = http_parser_parse_url(buff, len, is_connect, parsed_url); if (result) { lua_pushnil(L); return 1; } lua_createtable(L, 0 , 7); push_url_part(L, buff, parsed_url, UF_SCHEMA); push_url_part(L, buff, parsed_url, UF_HOST); push_url_part(L, buff, parsed_url, UF_PORT); push_url_part(L, buff, parsed_url, UF_PATH); push_url_part(L, buff, parsed_url, UF_QUERY); push_url_part(L, buff, parsed_url, UF_FRAGMENT); push_url_part(L, buff, parsed_url, UF_USERINFO); free(parsed_url); return 1; } static const struct luaL_Reg luaw_http_lib[] = { {"urlDecode", luaw_url_decode}, {"newHttpRequestParser", luaw_new_http_request_parser}, {"newHttpResponseParser", luaw_new_http_response_parser}, {"parseURL", luaw_parse_url}, {NULL, NULL} /* sentinel */ }; static const struct luaL_Reg http_parser_methods[] = { {"parseHttp", parse_http}, {"initHttpParser", luaw_init_http_parser}, {NULL, NULL} /* sentinel */ }; void luaw_init_http_lib (lua_State *L) { make_metatable(L, LUA_HTTP_PARSER_META_TABLE, http_parser_methods); luaL_newlib(L, luaw_http_lib); lua_setglobal(L, "luaw_http_lib"); } ================================================ FILE: src/luaw_http_parser.h ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "http_parser.h" #include "uv.h" #ifndef LUAW_HTTP_PARSER_H #define LUAW_HTTP_PARSER_H #define LUA_HTTP_PARSER_META_TABLE "__luaw_HTTP_parser_MT__" typedef enum { starting_name = 0, in_name, starting_value, in_value } decoder_state; typedef enum { http_cb_none = 1, //Used to denote invocations of http_parser_execute() that don't cause callback http_cb_on_message_begin, http_cb_on_status, http_cb_on_url, http_cb_on_header_field, http_cb_on_header_value, http_cb_on_headers_complete, http_cb_on_body, http_cb_on_mesg_complete } http_parser_cb_type; /* imported from http_parser.h */ static const char *http_methods[] = { #define XX(num, name, string) #string, HTTP_METHOD_MAP(XX) #undef XX }; typedef struct { http_parser parser; http_parser_cb_type http_cb; char* start; size_t len; } luaw_http_parser_t; extern void luaw_init_http_lib(lua_State *L); #endif ================================================ FILE: src/luaw_logging.c ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include #include #include "uv.h" #include "luaw_common.h" #include "luaw_logging.h" static char hostname[512] = {'\0'}; static logfile_sate log_state = LOG_NOT_OPEN; static uv_file logfile; static struct addrinfo *log_server_addr = NULL; static int log_sock_fd = -1; static const char* get_hostname() { if (hostname[0] == '\0') { int rc = gethostname(hostname, 511); if (rc < 0) { strcpy(hostname, "localhost"); } } return hostname; } LUA_LIB_METHOD static int get_hostname_lua(lua_State *L) { lua_pushstring(L, get_hostname()); return 1; } LUA_LIB_METHOD static int get_logging_state(lua_State* l_thread) { lua_pushinteger(l_thread, log_state); return 1; } LIBUV_CALLBACK static void on_log_open(uv_fs_t* req) { logfile = req->result; log_state = LOG_IS_OPEN; uv_fs_req_cleanup(req); free(req); } LUA_LIB_METHOD static int open_log_file(lua_State* l_thread) { if (log_state == LOG_NOT_OPEN) { const char* filename = lua_tostring(l_thread, 1); if (filename) { uv_fs_t* open_req = (uv_fs_t*)malloc(sizeof(uv_fs_t)); if (open_req) { uv_loop_t* loop = uv_default_loop(); int rc = uv_fs_open(loop, open_req, filename, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP, on_log_open); if (rc == 0) { log_state = OPENING_LOG; } } } } return 0; } LIBUV_CALLBACK static void on_log_close(uv_fs_t* req) { uv_fs_req_cleanup(req); free(req); } LIBUV_CALLBACK static void close_log(uv_fs_t* req) { uv_file f = *((int*)req->data); uv_fs_req_cleanup(req); uv_fs_close(uv_default_loop(), req, f, on_log_close); } LIBUV_CALLBACK static void on_log_write(uv_fs_t* req) { if (req->result >= 0) { uv_fs_req_cleanup(req); free(req); } else { /* error */ log_state = LOG_NOT_OPEN; close_log(req); } } LUA_LIB_METHOD static int write_log(lua_State* l_thread) { if (log_state == LOG_IS_OPEN) { size_t len = 0; const char* str = lua_tolstring(l_thread, 1, &len); if ((str != NULL)||(len > 0)) { char* log_mesg = malloc(len * sizeof(char)); if (log_mesg != NULL) { memcpy(log_mesg, str, len); uv_fs_t* write_req = (uv_fs_t*)malloc(sizeof(uv_fs_t)); if (write_req) { write_req->data = &logfile; uv_buf_t buff = uv_buf_init(log_mesg, len); uv_loop_t* loop = uv_default_loop(); int rotate_log = lua_toboolean(l_thread, 2); if (rotate_log == 0) { int rc = uv_fs_write(loop, write_req, logfile, &buff, 1, -1, on_log_write); if (rc != 0) { log_state = LOG_NOT_OPEN; close_log(write_req); } } else { log_state = LOG_NOT_OPEN; uv_fs_write(loop, write_req, logfile, &buff, 1, -1, close_log); } } else { free(log_mesg); } } } } lua_pushinteger(l_thread, log_state); return 1; } void close_syslog() { uv_freeaddrinfo(log_server_addr); } LUA_LIB_METHOD static int connect_to_syslog(lua_State *L) { int rc = -1; int flags = 1; const char* log_server_ip = lua_tostring(L, 1); const char* log_server_port = lua_tostring(L, 2); if (log_server_ip && log_server_port) { struct addrinfo hints; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = 0; hints.ai_protocol = 0; rc = getaddrinfo(log_server_ip, log_server_port, &hints, &log_server_addr); if (rc != 0) { fprintf(stderr,"failed to get address for sys log server :%s\n",gai_strerror(rc)); } else { rc = socket(log_server_addr->ai_family, log_server_addr->ai_socktype, log_server_addr->ai_protocol); if (rc < 0) { fprintf(stderr, "could not connect to sys log server\n"); } else { log_sock_fd = rc; #if defined(O_NONBLOCK) if (-1 == (flags = fcntl(log_sock_fd, F_GETFL, 0))) flags = 0; rc = fcntl(log_sock_fd, F_SETFL, flags | O_NONBLOCK); #else rc = ioctl(log_sock_fd, FIONBIO, &flags); #endif if (rc < 0) { fprintf(stderr, "Error putting syslog connection in non blocking mode\n"); } } } } lua_pushboolean(L, (rc < 0) ? 0 : 1); return 1; } LUA_LIB_METHOD static int send_to_syslog(lua_State *L) { if (log_sock_fd > 0) { size_t len = 0; const char* mesg = lua_tolstring(L, 1, &len); if ((mesg)&&(len > 0)) { sendto(log_sock_fd, mesg, len, MSG_DONTWAIT,log_server_addr->ai_addr, log_server_addr->ai_addrlen); } } return 0; } static const struct luaL_Reg luaw_logging_lib[] = { {"logState", get_logging_state}, {"openLog", open_log_file}, {"writeLog", write_log}, {"syslogConnect", connect_to_syslog}, {"syslogSend", send_to_syslog}, {"hostname", get_hostname_lua}, {NULL, NULL} /* sentinel */ }; int luaw_init_logging_lib (lua_State *L) { luaL_newlib(L, luaw_logging_lib); lua_setglobal(L, "luaw_logging_lib"); return 1; } ================================================ FILE: src/luaw_logging.h ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #ifndef LUAW_LOGGING_H #define LUAW_LOGGING_H typedef enum { LOG_NOT_OPEN = 0, OPENING_LOG, LOG_IS_OPEN, } logfile_sate; extern void close_syslog(); extern int luaw_init_logging_lib (lua_State *L); #endif ================================================ FILE: src/luaw_server.c ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "uv.h" #include "luaw_common.h" #include "luaw_logging.h" #include "luaw_tcp.h" #include "lfs.h" static char* server_ip = "0.0.0.0"; static int server_port = 80; static uv_tcp_t server; static uv_loop_t* event_loop; static uv_signal_t shutdown_signal; static int service_http_fn_ref; static int start_thread_fn_ref; static int run_ready_threads_fn_ref; #define LUA_LOAD_FILE_BUFF_SIZE 1024 typedef struct { FILE *file; /* file being read */ char buff[LUA_LOAD_FILE_BUFF_SIZE]; /* area for reading file */ char* epilogue; } lua_load_buffer_t; uv_prepare_t user_thread_runner; static const char* lua_file_reader(lua_State* L, void* data, size_t* size) { lua_load_buffer_t *lb = (lua_load_buffer_t*)data; if (lb->file == NULL) return NULL; if (feof(lb->file)) { fclose(lb->file); lb->file = NULL; *size = (lb->epilogue != NULL) ? strlen(lb->epilogue) : 0; return lb->epilogue; } *size = fread(lb->buff, 1, sizeof(lb->buff), lb->file); /* read block */ return lb->buff; } static void handle_shutdown_req(uv_signal_t* handle, int signum) { if (signum == SIGHUP) { fprintf(stderr, "shutdown request received\n"); uv_signal_stop(handle); uv_stop(event_loop); } } void init_luaw_server(lua_State* L) { lua_getglobal(L, "luaw_http_lib"); if (!lua_istable(L, -1)) { fprintf(stderr, "Luaw HTTP library not initialized\n"); exit(EXIT_FAILURE); } lua_getfield(L, -1, "request_handler"); if (!lua_isfunction(L, -1)) { fprintf(stderr, "Main HTTP request handler function (Luaw.request_handler) not set\n"); exit(EXIT_FAILURE); } service_http_fn_ref = luaL_ref(L, LUA_REGISTRYINDEX); lua_pop(L, 1); lua_getglobal(L, "luaw_scheduler"); if (!lua_istable(L, -1)) { fprintf(stderr, "Luaw scheduler not initialized\n"); exit(EXIT_FAILURE); } lua_getfield(L, -1, "resumeThreadId"); if (lua_isfunction(L, -1)) { resume_thread_fn_ref = luaL_ref(L, LUA_REGISTRYINDEX); } else { fprintf(stderr, "resumeThreadId function not found in luaw scheduler\n"); exit(EXIT_FAILURE); } lua_getfield(L, -1, "startSystemThread"); if (lua_isfunction(L, -1)) { start_thread_fn_ref = luaL_ref(L, LUA_REGISTRYINDEX); } else { fprintf(stderr, "startSystemThread function not found in luaw scheduler\n"); exit(EXIT_FAILURE); } lua_getfield(L, -1, "runReadyThreads"); if (lua_isfunction(L, -1)) { run_ready_threads_fn_ref = luaL_ref(L, LUA_REGISTRYINDEX); } else { fprintf(stderr, "runReadyThreads function not found in luaw scheduler\n"); exit(EXIT_FAILURE); } lua_pop(L, 1); lua_getglobal(L, "luaw_server_config"); if (lua_istable(L, -1)) { lua_getfield(L, -1, "server_ip"); if (lua_isstring(L, -1)) { server_ip = (char *)lua_tostring(L, -1); lua_pop(L, 1); } lua_getfield(L, -1, "server_port"); if (lua_isnumber(L, -1)) { server_port = lua_tointeger(L, -1); lua_pop(L, 1); } lua_pop(L, 1); //pop luaw_server_config object } lua_pushnumber(L, CONN_BUFFER_SIZE); lua_setglobal(L, "CONN_BUFFER_SIZE"); event_loop = uv_default_loop(); } /* create a new lua coroutine to service this conn, anchor it in "all active coroutines" * global table to prevent it from being garbage collected and start the coroutine */ LIBUV_CALLBACK static void on_server_connect(uv_stream_t* server, int status) { if (status) { raise_lua_error(l_global, "Error in on_server_connect callback: %s\n", uv_strerror(status)); return; } lua_rawgeti(l_global, LUA_REGISTRYINDEX, start_thread_fn_ref); assert(lua_isfunction(l_global, -1)); lua_rawgeti(l_global, LUA_REGISTRYINDEX, service_http_fn_ref); assert(lua_isfunction(l_global, -1)); connection_t * conn = new_connection(l_global); status = uv_accept(server, (uv_stream_t*)&conn->handle); if (status) { close_connection(conn, status); fprintf(stderr, "Error accepting incoming conn: %s\n", uv_strerror(status)); return; } status = lua_pcall(l_global, 2, 2, 0); if (status) { fprintf(stderr, "**** Error starting new client connect thread: %s (%d) ****\n", lua_tostring(l_global, -1), status); } } void start_server(lua_State *L) { fprintf(stderr, "starting server on port %d ...\n", server_port); struct sockaddr_in addr; int err_code = uv_ip4_addr(server_ip, server_port, &addr); if (err_code) { fprintf(stderr, "Error initializing socket address: %s\n", uv_strerror(err_code)); exit(EXIT_FAILURE); } uv_tcp_init(event_loop, &server); err_code = uv_tcp_bind(&server, (const struct sockaddr*) &addr, 0); if (err_code) { fprintf(stderr, "Error binding to port %d : %s\n", server_port, uv_strerror(err_code)); exit(EXIT_FAILURE); } err_code = uv_listen((uv_stream_t*)&server, 128, on_server_connect); if (err_code) { fprintf(stderr, "Error listening on port %d : %s\n", server_port, uv_strerror(err_code)); exit(EXIT_FAILURE); } uv_signal_init(event_loop, &shutdown_signal); uv_signal_start(&shutdown_signal, handle_shutdown_req, SIGHUP); } static void run_user_threads(uv_prepare_t* handle) { /* do the bottom half processing, run ready user threads that are waiting */ lua_rawgeti(l_global, LUA_REGISTRYINDEX, run_ready_threads_fn_ref); int status = lua_pcall(l_global, 0, 1, 0); if (status != LUA_OK) { fprintf(stderr,"Error while running user threads for bottom half processing: %s\n", lua_tostring(l_global, -1)); uv_stop(event_loop); } lua_settop(l_global, 0); } static void close_walk_cb(uv_handle_t* handle, void* arg) { if (!uv_is_closing(handle)) { uv_close(handle, NULL); } } static int server_loop(lua_State *L) { uv_prepare_init(event_loop, &user_thread_runner); uv_prepare_start(&user_thread_runner, run_user_threads); int status = uv_run(event_loop, UV_RUN_DEFAULT); /* clean up resources used by the event loop and Lua */ uv_walk(event_loop, close_walk_cb, NULL); uv_run(event_loop, UV_RUN_ONCE); uv_loop_delete(event_loop); lua_close(L); close_syslog(); return status; } static void run_lua_file(const char* filename, char* epilogue) { lua_load_buffer_t lb; lb.file = fopen(filename, "r"); if (lb.file == NULL) { fprintf(stderr, "Could not open file %s for reading\n", filename); exit(EXIT_FAILURE); } lb.epilogue = epilogue; #ifdef COMPAT52_IS_LUAJIT int status = lua_load(l_global, lua_file_reader, &lb, filename); #else int status = lua_load(l_global, lua_file_reader, &lb, filename, "t"); #endif if (status != LUA_OK) { fprintf(stderr, "Error while loading file: %s\n", filename); fprintf(stderr, "%s\n", lua_tostring(l_global, -1)); exit(EXIT_FAILURE); } status = lua_pcall(l_global, 0, 0, 0); if (status != LUA_OK) { fprintf(stderr, "Error while executing file: %s\n", filename); fprintf(stderr, "%s\n", lua_tostring(l_global, -1)); fprintf(stderr, "\n\t* Try running luaw_server from directory that contains \"bin\" directory containing the binary \"luaw_server\"\n"); fprintf(stderr, "\t* For example: ./bin/luaw_server \n\n"); exit(EXIT_FAILURE); } } static void set_lua_path(lua_State* L) { lua_getglobal( L, "package" ); lua_pushliteral(L, "?;?.lua;./bin/?;./bin/?.lua;./lib/?;./lib/?.lua"); lua_setfield( L, -2, "path" ); lua_pop(L, 1); } int main (int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s \n", argv[0]); exit(EXIT_FAILURE); } l_global = luaL_newstate(); if (!l_global) { fprintf(stderr, "Could not create new Lua state\n"); exit(EXIT_FAILURE); } lua_gc(l_global, LUA_GCSTOP, 0); /* stop collector during initialization */ luaL_openlibs(l_global); /* open libraries */ luaw_init_libs(l_global); luaopen_lfs(l_global); lua_gc(l_global, LUA_GCRESTART, 0); /* load config file, mandatory */ set_lua_path(l_global); run_lua_file(argv[1], "\ninit = require(\"luaw_init\")\n"); /* run other lua on startup script passed on the command line, if any */ int i = 2; for (; i < argc; i++) { fprintf(stderr, "## Running %s \n", argv[i]); run_lua_file(argv[i], NULL); } init_luaw_server(l_global); start_server(l_global); int status = server_loop(l_global); exit(status); } ================================================ FILE: src/luaw_tcp.c ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include "uv.h" #include "luaw_common.h" #include "http_parser.h" #include "luaw_http_parser.h" #include "luaw_tcp.h" #include "lfs.h" connection_t* new_connection(lua_State* L) { connection_t* conn = (connection_t*)calloc(1, sizeof(connection_t)); if (conn == NULL) { raise_lua_error(L, "Could not allocate memory for client connection"); return NULL; } connection_t** lua_ref = lua_newuserdata(L, sizeof(connection_t*)); if (lua_ref == NULL) { free(conn); raise_lua_error(L, "Could not allocate memory for client connection Lua reference"); return NULL; } /* link C side connection reference and Lua's full userdata that represents it to each other */ luaL_setmetatable(L, LUA_CONNECTION_META_TABLE); *lua_ref = conn; INCR_REF_COUNT(conn) conn->lua_ref = lua_ref; /* init libuv artifacts */ uv_tcp_init(uv_default_loop(), &conn->handle); conn->handle.data = conn; INCR_REF_COUNT(conn) uv_timer_init(uv_default_loop(), &conn->read_timer); conn->read_timer.data = conn; INCR_REF_COUNT(conn) conn->read_len = 0; conn->lua_reader_tid = 0; uv_timer_init(uv_default_loop(), &conn->write_timer); conn->write_timer.data = conn; INCR_REF_COUNT(conn) conn->lua_writer_tid = 0; return conn; } LUA_LIB_METHOD static int new_connection_lua(lua_State* L) { new_connection(L); return 1; } static void free_timer(uv_handle_t* handle) { connection_t* conn = GET_CONN_OR_RETURN(handle); handle->data = NULL; GC_REF(conn) } static void free_tcp_handle(uv_handle_t* handle) { connection_t* conn = GET_CONN_OR_RETURN(handle); handle->data = NULL; GC_REF(conn) } void close_connection(connection_t* conn, const int status) { /* conn->lua_ref == NULL also acts as a flag to mark that this conn has been closed */ if ((conn == NULL)||(conn->lua_ref == NULL)) return; *(conn->lua_ref) = NULL; //delink from Lua's userdata conn->lua_ref = NULL; DECR_REF_COUNT(conn); uv_timer_stop(&conn->read_timer); close_if_active((uv_handle_t*)&conn->read_timer, (uv_close_cb)free_timer); uv_timer_stop(&conn->write_timer); close_if_active((uv_handle_t*)&conn->write_timer, (uv_close_cb)free_timer); close_if_active((uv_handle_t*)&conn->handle, (uv_close_cb)free_tcp_handle); /* unblock reader thread */ if (conn->lua_reader_tid) { lua_rawgeti(l_global, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_global, conn->lua_reader_tid); if ((status == 0)||(status == UV_EOF)) { lua_pushboolean(l_global, 0); lua_pushliteral(l_global, "EOF"); } else { /* error */ lua_pushboolean(l_global, 0); lua_pushstring(l_global, uv_strerror(status)); } conn->lua_reader_tid = 0; resume_lua_thread(l_global, 3, 2, 0); } /* unblock writer thread */ if (conn->lua_writer_tid) { lua_rawgeti(l_global, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_global, conn->lua_writer_tid); if ((status == 0)||(status == UV_EOF)) { lua_pushboolean(l_global, 1); lua_pushinteger(l_global, 0); } else { /* error */ lua_pushboolean(l_global, 0); lua_pushstring(l_global, uv_strerror(status)); } conn->lua_writer_tid = 0; resume_lua_thread(l_global, 3, 2, 0); } } LIBUV_CALLBACK static void on_conn_timeout(uv_timer_t* timer) { /* Either connect,read or write timed out, close the connection */ connection_t* conn = GET_CONN_OR_RETURN(timer); close_connection(conn, UV_ECANCELED); } static int start_timer(uv_timer_t* timer, int timeout) { if ((timeout > 0)&&(!uv_is_active((uv_handle_t*) timer))) { return uv_timer_start(timer, on_conn_timeout, timeout, 0); } return 0; } static void stop_timer(uv_timer_t* timer) { if (uv_is_active((uv_handle_t*) timer)) { uv_timer_stop(timer); } } LUA_OBJ_METHOD static int close_connection_lua(lua_State* l_thread) { LUA_GET_CONN_OR_RETURN(l_thread, 1, conn); /* being called from lua, no reason to resume thread */ conn->lua_reader_tid = 0; conn->lua_writer_tid = 0; close_connection(conn, UV_EOF); return 0; } LUA_OBJ_METHOD static int connection_gc(lua_State *L) { LUA_GET_CONN_OR_RETURN(L, 1, conn); /* if we reached here, there is a connection that has not been closed */ fprintf(stderr, "Luaw closed unclosed connection\n"); /* being called from lua, no reason to resume thread */ conn->lua_reader_tid = 0; conn->lua_writer_tid = 0; close_connection(conn, UV_ECANCELED); return 0; } /* reuse the buffer attached with this conn to minimize memory allocation. Each on_read() * resets the buffer to empty after sending all the bytes read to the coroutine servicing this * conn. If we get called before on_read() has had chance to empty the buffer, we return * 0 which means on_read() will be called with nread=UV_ENOBUFS next which we must handle. */ LIBUV_CALLBACK static void on_alloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { buf->base = NULL; buf->len = 0; connection_t* conn = GET_CONN_OR_RETURN(handle); if(conn->read_buffer) { size_t free_space = CONN_BUFFER_SIZE - conn->read_len; if (free_space > 0) { buf->base = conn->read_buffer + conn->read_len; buf->len = free_space; } } } /* Returns * 1. tid * 2. status: true = succesfull read, false = error * 3. string = read string for succesfull read, error message for failure */ LIBUV_API static void on_read(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { connection_t* conn = GET_CONN_OR_RETURN(stream); stop_timer(&conn->read_timer); //clear read timeout if any if ((nread == 0)||(nread == UV_ENOBUFS)) { /* either no data was read or no buffer was available to read the data. Anyway there is nothing to do so no need to wake conn coroutine. Let this callback pass through as NOOP */ return; } if (nread > 0) { /* success: send read bytes to coroutine if one is waiting */ lua_rawgeti(l_global, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_global, conn->lua_reader_tid); lua_pushboolean(l_global, 1); lua_pushlstring(l_global, conn->read_buffer, (conn->read_len + nread)); conn->lua_reader_tid = 0; conn->read_len = 0L; resume_lua_thread(l_global, 3, 2, 0); return; } /* either EOF or read error, in either case close connection */ close_connection(conn, nread); } /* lua call spec: Success: status(true), nil = conn:start_reading() Failure: status(false), error message = conn:start_reading() */ LUA_OBJ_METHOD static int start_reading(lua_State *l_thread) { LUA_GET_CONN_OR_ERROR(l_thread, 1, conn); conn->lua_reader_tid = 0; int err_code = uv_read_start((uv_stream_t*)&conn->handle, on_alloc, on_read); if (err_code) { lua_pushboolean(l_thread, 0); lua_pushstring(l_thread, uv_strerror(err_code)); close_connection(conn, err_code); return 2; } lua_pushboolean(l_thread, 1); return 1; } /* Returns * 1. status: true for success or no data, false for error * 2. str: read string for successful read, NULL if no data, error message for failure */ LUA_OBJ_METHOD static int read_check(lua_State* l_thread) { LUA_GET_CONN_OR_ERROR(l_thread, 1, conn); if (!uv_is_active((uv_handle_t*)&conn->handle)) { close_connection(conn, UV_EAI_BADFLAGS); return error_to_lua(l_thread, "read() called on conn that is not registered to receive read events"); } if (conn->read_len == 0) { /* empty buffer, record reader tid and block (yield) in lua */ int lua_reader_tid = lua_tointeger(l_thread, 2); if (lua_reader_tid == 0) { return error_to_lua(l_thread, "read() specified invalid thread id"); } conn->lua_reader_tid = lua_reader_tid; int readTimeout = lua_tointeger(l_thread, 3); start_timer(&conn->read_timer, readTimeout); lua_pushboolean(l_thread, 1); lua_pushnil(l_thread); return 2; } /* data available in buffer */ lua_pushboolean(l_thread, 1); lua_pushlstring(l_thread, conn->read_buffer, conn->read_len); return 2; } LIBUV_API static void on_write(uv_write_t* req, int status) { connection_t* conn = TO_CONN(req); if(conn) { req->data = NULL; if (status) { close_connection(conn, status); } else { stop_timer(&conn->write_timer); //clear write timeout if any lua_rawgeti(l_global, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_global, conn->lua_writer_tid); conn->lua_writer_tid = 0; lua_pushboolean(l_global, 1); lua_pushinteger(l_global, 1); resume_lua_thread(l_global, 3, 2, 0); } /* Unlike libuv handles, libuv requests do not support uv_close(). Therefore we increment reference count every time request starts and decrement it as soon as it completes */ GC_REF(conn) } } /* lua call spec: conn:write(tid, str, writeTimeout) Success: status(true), nwritten Failure: status(false), error message */ LUA_OBJ_METHOD static int write_buffer(lua_State* l_thread) { LUA_GET_CONN_OR_ERROR(l_thread, 1, conn); int lua_writer_tid = lua_tointeger(l_thread, 2); if (lua_writer_tid == 0) { return error_to_lua(l_thread, "write() specified invalid thread id"); } size_t len = 0; const char* buff = lua_tolstring(l_thread, 3, &len); if (len > 0) { /* non empty write buffer. Send write request, record writer tid and block in lua */ int writeTimeout = lua_tointeger(l_thread, 4); uv_buf_t write_buff; write_buff.base = (char*) buff; write_buff.len = len; int err_code = uv_write(&conn->write_req, (uv_stream_t*)&conn->handle, &write_buff, 1, on_write); if (err_code) { close_connection(conn, err_code); lua_pushboolean(l_thread, 0); lua_pushstring(l_thread, uv_strerror(err_code)); return 2; } conn->write_req.data = conn; INCR_REF_COUNT(conn) conn->lua_writer_tid = lua_writer_tid; start_timer(&conn->write_timer, writeTimeout); } lua_pushboolean(l_thread, 1); lua_pushinteger(l_thread, len); return 2; } LIBUV_CALLBACK static void on_client_connect(uv_connect_t* connect_req, int status) { connection_t* conn = GET_CONN_OR_RETURN(connect_req); stop_timer(&conn->write_timer); //clear connect timeout if any free(connect_req); lua_rawgeti(l_global, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_global, conn->lua_writer_tid); if (status) { close_connection(conn, status); lua_pushboolean(l_global, 0); lua_pushstring(l_global, uv_strerror(status)); } else { lua_pushboolean(l_global, 1); //status to be returned lua_pushnil(l_global); } resume_lua_thread(l_global, 3, 2, 0); } /* lua call spec: luaw_lib.connect(ip4_addr, port, tid, connectTimeout) Success: conn Failure: false, error message */ LUA_LIB_METHOD static int client_connect(lua_State* l_thread) { const char* ip4 = luaL_checkstring(l_thread, 1); int port = lua_tointeger(l_thread, 2); if (port == 0) { return error_to_lua(l_thread, "Invalid port specified in client_connect"); } int tid = lua_tointeger(l_thread, 3); if (tid == 0) { return error_to_lua(l_thread, "Invalid thread id specified in client_connect"); } int connectTimeout = lua_tointeger(l_thread, 4); struct sockaddr_in addr; if(uv_ip4_addr(ip4, port, &addr)) { return error_to_lua(l_thread, "Invalid ip address %s and port %d combination specified in client_connect", ip4, port); } connection_t* conn = new_connection(l_thread); uv_connect_t* connect_req = (uv_connect_t*)malloc(sizeof(uv_connect_t)); if (connect_req == NULL) { close_connection(conn, UV_ENOMEM); return error_to_lua(l_thread, "Could not allocate memory for connect request"); } connect_req->data = conn; int status = uv_tcp_connect(connect_req, &conn->handle, (const struct sockaddr*) &addr, on_client_connect); if (status) { free(connect_req); close_connection(conn, status); return error_to_lua(l_thread, "tcp connect failed: %s", uv_strerror(status)); } conn->lua_writer_tid = tid; start_timer(&conn->write_timer, connectTimeout); return 1; } LIBUV_CALLBACK static void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) { int lua_tid = *((int *)resolver->data); free(resolver->data); free(resolver); char ip_str[17] = {'\0'}; if ((status == 0)&&(res != NULL)) { struct sockaddr_in addr = *(struct sockaddr_in*) res->ai_addr; status = uv_ip4_name(&addr, (char*)ip_str, sizeof(ip_str)); uv_freeaddrinfo(res); } lua_rawgeti(l_global, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_global, lua_tid); if ((status)||(res == NULL)) { lua_pushboolean(l_global, 0); lua_pushfstring(l_global, "DNS resolution failed: %s", uv_strerror(status)); } else { lua_pushboolean(l_global, 1); //status to be returned lua_pushstring(l_global, ip_str); //IP address string } resume_lua_thread(l_global, 3, 2, 0); } LUA_LIB_METHOD static int dns_resolve(lua_State* l_thread) { const char* hostname = luaL_checkstring(l_thread, 1); int lua_tid = lua_tointeger(l_thread, 2); if (lua_tid == 0) { return error_to_lua(l_thread, "Invalid thread id specified in dns_resolve"); } struct addrinfo hints; hints.ai_family = PF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = 0; uv_getaddrinfo_t* resolver = (uv_getaddrinfo_t*)malloc(sizeof(uv_getaddrinfo_t)); if (resolver == NULL) { return error_to_lua(l_thread, "Could not allocate memory for DNS resolver"); } int* tid = (int*)malloc(sizeof(int)); if (tid == NULL) { free(resolver); return error_to_lua(l_thread, "Could not allocate memory thread id"); } *tid = lua_tid; resolver->data = tid; int status = uv_getaddrinfo(uv_default_loop(), resolver, on_resolved, hostname, NULL, &hints); if (status) { free(resolver); free(tid); return error_to_lua(l_thread, "DNS resolve failed: %s", uv_strerror(status)); } //success lua_pushboolean(l_thread, 1); return 1; } static const struct luaL_Reg luaw_connection_methods[] = { {"startReading", start_reading}, {"read", read_check}, {"write", write_buffer}, {"close", close_connection_lua}, {"__gc", connection_gc}, {NULL, NULL} /* sentinel */ }; static const struct luaL_Reg luaw_tcp_lib[] = { {"newConnection", new_connection_lua}, {"connect", client_connect}, {"resolveDNS", dns_resolve}, {NULL, NULL} /* sentinel */ }; void luaw_init_tcp_lib (lua_State *L) { make_metatable(L, LUA_CONNECTION_META_TABLE, luaw_connection_methods); luaL_newlib(L, luaw_tcp_lib); lua_setglobal(L, "luaw_tcp_lib"); } ================================================ FILE: src/luaw_tcp.h ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #ifndef LUAW_TCP_H #define LUAW_TCP_H #define LUA_CONNECTION_META_TABLE "_luaw_connection_MT_" #define CONN_BUFFER_SIZE 4096 typedef struct connection_s connection_t; /* client connection's state: socket connection, coroutines servicing the connection and read/write buffers for the connection */ struct connection_s { uv_tcp_t handle; /* connected socket */ /* read section */ int lua_reader_tid; /* ID of the reading coroutine */ size_t read_len; /* read length */ uv_timer_t read_timer; /* for read timeout */ /* write section */ int lua_writer_tid; /* ID of the writing coroutine */ uv_timer_t write_timer; /* for write/connect timeout */ uv_write_t write_req; /* write request */ /* memory management */ int ref_count; /* reference count */ connection_t** lua_ref; /* back reference to Lua's full userdata pointing to this conn */ /* read buffer */ char read_buffer[CONN_BUFFER_SIZE]; /* buffer to read into */ }; #define MAX_CONNECTION_BUFF_SIZE 65536 //16^4 #define TO_CONN(h) (connection_t*)h->data #define GET_CONN_OR_RETURN(h) \ (connection_t*)h->data; \ if(!h->data) return #define LUA_GET_CONN_OR_RETURN(L, i, c) \ connection_t** cr = luaL_checkudata(L, i, LUA_CONNECTION_META_TABLE); \ if (cr == NULL) return 0; \ connection_t* c = *cr; \ if (c == NULL) return 0; #define LUA_GET_CONN_OR_ERROR(L, i, c) \ connection_t** cr = luaL_checkudata(L, i, LUA_CONNECTION_META_TABLE); \ if (cr == NULL) return error_to_lua(L, "Connection missing"); \ connection_t* c = *cr; \ if (c == NULL) return error_to_lua(L, "Connection closed"); #define TO_TIMER(h) (luaw_timer_t*)h->data /* TCP lib methods to be exported */ extern connection_t* new_connection(lua_State* L); extern void close_connection(connection_t* conn, const int status); extern void luaw_init_tcp_lib (lua_State *L); #endif ================================================ FILE: src/luaw_timer.c ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include "uv.h" #include "luaw_common.h" #include "luaw_timer.h" static void clear_user_timer(luaw_timer_t* timer) { timer->state = INIT; timer->lua_tid = 0; } static void free_user_timer(uv_handle_t* handle) { luaw_timer_t* timer = GET_TIMER_OR_RETURN(handle); handle->data = NULL; GC_REF(timer) } LUA_OBJ_METHOD static void close_timer(luaw_timer_t* timer) { /* timer->lua_ref == NULL also acts as a flag to mark that this timer has been closed */ if ((timer == NULL)||(timer->lua_ref == NULL)) return; *(timer->lua_ref) = NULL; //delink from Lua's userdata timer->lua_ref = NULL; DECR_REF_COUNT(timer); /* unblock waiting thread */ if (timer->lua_tid) { lua_rawgeti(l_global, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_global, timer->lua_tid); lua_pushboolean(l_global, 0); //status lua_pushstring(l_global, uv_strerror(UV_ECANCELED)); //error message timer->lua_tid = 0; resume_lua_thread(l_global, 3, 2, 0); } close_if_active((uv_handle_t*)&timer->handle, free_user_timer); } LUA_OBJ_METHOD static int close_timer_lua(lua_State* l_thread) { LUA_GET_TIMER_OR_RETURN(l_thread, 1, timer); close_timer(timer); return 0; } LUA_OBJ_METHOD static int timer_gc(lua_State *L) { LUA_GET_TIMER_OR_RETURN(L, 1, timer); close_timer(timer); return 0; } LUA_LIB_METHOD static int new_user_timer(lua_State* l_thread) { luaw_timer_t* timer = (luaw_timer_t*)calloc(1, sizeof(luaw_timer_t)); if (timer == NULL) { return raise_lua_error(l_thread, "Could not allocate memory for user timer"); } luaw_timer_t** lua_ref = lua_newuserdata(l_thread, sizeof(luaw_timer_t*)); if (lua_ref == NULL) { free(timer); return raise_lua_error(l_thread, "Could not allocate memory for user timer Lua reference"); } /* link C side connection reference and Lua's full userdata that represents it to each other */ luaL_setmetatable(l_thread, LUA_USER_TIMER_META_TABLE); *lua_ref = timer; INCR_REF_COUNT(timer) timer->lua_ref = lua_ref; /* init libuv artifacts */ uv_timer_init(uv_default_loop(), &timer->handle); timer->handle.data = timer; INCR_REF_COUNT(timer) clear_user_timer(timer); return 1; } /* lua call spec: status, timer_elapsed = timer:wait(tid) Success, timer elapsed: status = true, timer_elapsed = true Success, timer not elapsed: status = true, timer_elapsed = false Failure: status = false, error message */ LIBUV_CALLBACK static void on_user_timer_timeout(uv_timer_t* handle) { luaw_timer_t* timer = GET_TIMER_OR_RETURN(handle); if(timer->lua_tid) { lua_rawgeti(l_global, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_global, timer->lua_tid); clear_user_timer(timer); lua_pushboolean(l_global, 1); //status lua_pushboolean(l_global, 1); //elapsed resume_lua_thread(l_global, 3, 2, 0); } else { timer->state = ELAPSED; } } /* lua call spec: Success: status(true), nil = timer:start(timeout) Failure: status(false), error message = timer:start(timeout) */ static int start_user_timer(lua_State* l_thread) { LUA_GET_TIMER_OR_ERROR(l_thread, 1, timer); if (timer->state != INIT) { /* Timer is already started by another lua thread */ return error_to_lua(l_thread, "Timer already in use by another thread"); } int timeout = lua_tointeger(l_thread, 2); if (timeout <= 0) { return error_to_lua(l_thread, "Invalid timeout value %d specified", timeout); } int rc = uv_timer_start(&timer->handle, on_user_timer_timeout, timeout, 0); if (rc) { close_timer(timer); return error_to_lua(l_thread, "Error starting timer: %s", uv_strerror(rc)); } timer->state = TICKING; lua_pushboolean(l_thread, 1); return 1; } /* lua call spec: status, timer_elapsed = timer:wait(tid) Success, timer elapsed: status = true, timer_elapsed = true Success, timer not elapsed: status = true, timer_elapsed = false Failure: status = false, error message */ LUA_OBJ_METHOD static int wait_user_timer(lua_State* l_thread) { LUA_GET_TIMER_OR_ERROR(l_thread, 1, timer); if (timer->state == ELAPSED) { clear_user_timer(timer); lua_pushboolean(l_thread, 1); //status lua_pushboolean(l_thread, 1); //elasped } else { if (timer->state != TICKING) { return error_to_lua(l_thread, "Attempt to wait on timer that is not started, timer state: %d", timer->state); } if (timer->lua_tid) { return error_to_lua(l_thread, "Timer already is in use by thread %d", timer->lua_tid); } int tid = lua_tointeger(l_thread, 2); if (tid <= 0) { return error_to_lua(l_thread, "Invalid thread id %d specified", tid); } timer->lua_tid = tid; lua_pushboolean(l_thread, 1); lua_pushboolean(l_thread, 0); } return 2; } /* lua call spec: timer:stop() */ LUA_OBJ_METHOD static int stop_user_timer(lua_State* l_thread) { LUA_GET_TIMER_OR_ERROR(l_thread, 1, timer); if (timer->state == TICKING) { if (timer->lua_tid) { lua_rawgeti(l_thread, LUA_REGISTRYINDEX, resume_thread_fn_ref); lua_pushinteger(l_thread, timer->lua_tid); lua_pushboolean(l_thread, 0); //status lua_pushstring(l_thread, uv_strerror(UV_ECANCELED)); //error message resume_lua_thread(l_thread, 3, 2, 0); } clear_user_timer(timer); uv_timer_stop(&timer->handle); } return 0; } static const struct luaL_Reg luaw_user_timer_methods[] = { {"start", start_user_timer}, {"stop", stop_user_timer}, {"wait", wait_user_timer}, {"delete", close_timer_lua}, {"__gc", timer_gc}, {NULL, NULL} /* sentinel */ }; static const struct luaL_Reg luaw_timer_lib[] = { {"newTimer", new_user_timer}, {NULL, NULL} /* sentinel */ }; void luaw_init_timer_lib (lua_State *L) { make_metatable(L, LUA_USER_TIMER_META_TABLE, luaw_user_timer_methods); luaL_newlib(L, luaw_timer_lib); lua_setglobal(L, "luaw_timer_lib"); } ================================================ FILE: src/luaw_timer.h ================================================ /* * Copyright (c) 2015 raksoras * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #ifndef LUAW_TIMER_H #define LUAW_TIMER_H #define LUA_USER_TIMER_META_TABLE "_luaw_user_timer_MT_" typedef enum { INIT = 0, TICKING, ELAPSED } timer_state; typedef struct luaw_timer_s luaw_timer_t; struct luaw_timer_s { uv_timer_t handle; /* timer handler */ timer_state state; /* timer state */ int lua_tid; /* id of a lua thread waiting on this timer */ /* memory management */ int ref_count; /* reference count */ luaw_timer_t** lua_ref; /* back reference to Lua's full userdata pointing to this conn */ }; #define TO_TIMER(h) (luaw_timer_t*)h->data #define GET_TIMER_OR_RETURN(h) \ (luaw_timer_t*)h->data; \ if(!h->data) return #define LUA_GET_TIMER_OR_RETURN(L, i, t) \ luaw_timer_t** tr = luaL_checkudata(L, i, LUA_USER_TIMER_META_TABLE); \ if (tr == NULL) return 0; \ luaw_timer_t* t = *tr; \ if (t == NULL) return 0; #define LUA_GET_TIMER_OR_ERROR(L, i, t) \ luaw_timer_t** tr = luaL_checkudata(L, i, LUA_USER_TIMER_META_TABLE); \ if (tr == NULL) return error_to_lua(L, "Timer missing"); \ luaw_timer_t* t = *tr; \ if (t == NULL) return error_to_lua(L, "Timer closed"); extern void luaw_init_timer_lib(lua_State *L); #endif