📜 ⬆️ ⬇️

Ruby integration into Nginx



For a long time there is a well-known bundle of Nginx + Lua, including a number of articles here. But time does not stand still. About a year ago, the first version of the module that integrates Ruby into Nginx appeared.

MRuby


For the integration, not a full-fledged Ruby was chosen, but its subset, which is intended to be embedded in other applications, devices, and so on. It has some limitations, but otherwise full-featured Ruby. The project is called MRuby . Currently it has version 1.0.0, i.e. considered stable.
MRuby does not allow other files to be connected at runtime, so the entire program must be in one file. At the same time, it is possible to convert the program into bytecode and execute it already, which has a positive effect on performance.
Since there is no possibility to load other files, then existing gems are not suitable for it. To extend the functionality, its own format is used, which is both C code and Ruby in places. These modules are assembled with the library itself at compile time and are an integral part of it. There are binding to various databases, to work with files, network, and so on. The full list is available on the website.
Also there is a module that allows you to integrate this engine in Nginx, which is particularly interested.

ngx_mruby


So, get acquainted: ngx_mruby . A module for connecting ruby ​​scripts to nginx. It has similar functionality with the Lua version. Allows you to perform operations at various stages of processing a request.
')
The module is going quite simply, there is a detailed instruction on the site. Who does not want to bother with the assembly, can download the finished package:
http://mruby.ajieks.ru/st/nginx_1.4.4-1~mruby~precise_amd64.deb
MRuby in this assembly contains the following additional modules:

As you can see, there is almost everything you need for work. The only thing that was not found in the API of this module is the ability to make a request out. Most likely, you will need to implement it as an extension and make a binding around the nginx API.

The author shows a beautiful graph with tests, but he did not find the environment configuration. So just put it for beauty:
image

Let's try to use


So, the server is already installed. Everything functions, static is given. Add a little to this dynamics.
As an example, I chose the task of parsing Markdown markup and return it to HTML without an additional server application. As well as line numbers in the source code for Ruby.
For this, a sinatra clone repository was made and nginx was configured to solve the problem.

Markdown

To handle the markup, we use the mruby-discount module that is connected to the assembly. It provides a simple class for working with markup. It is based on the C library of the same name, so I think it will not be a question of productivity.
To begin with, we will write a program that will read the requested file from the disk, process it and give it to the user.
r = Nginx::Request.new m = Discount.new("/st/style.css", "README") filename = r.filename filename = File.join(filename, 'README.md') if filename.end_with?('/') markdown = File.exists?(filename) ? File.read(filename) : '' Nginx.rputs m.header Nginx.rputs m.md2html(markdown) Nginx.rputs m.footer 

The first line is an instance of the request object, containing all the necessary information, including the requested file, headers, URLs, URIs, etc.
The next line creates an instance of the Discount class, indicating the style file and the page title.
This code does not handle 404 errors, so even if there is no file, there will always be a 200 return code.
Now we connect all this
  location ~ \.md$ { add_header Content-Type text/html; mruby_content_handler "/opt/app/parse_md.rb" cache; } 

Result:
mruby.ajieks.ru/sinatra
mruby.ajieks.ru/sinatra/README.ru.md

Ruby files

Originally I planned to do more than just numbering, as well as coloring the code using the once written code https://github.com/fuCtor/chalks . However, after all the adaptations made, problems arose in his work. The code, it seems, worked, but at a certain stage fell from the Segmentation fault. The initial suspicion was the lack of memory allocated, but even after reducing its consumption, the problem did not disappear. After removing the code associated with the coloring, everything worked, but not as beautiful as we wanted.
Result of change
 module CGI TABLE_FOR_ESCAPE_HTML__ = {"&"=>"&", '"'=>""", "<"=>"<", ">"=>">"} def self.escapeHTML(string) string.gsub(/[&\"<>]/) do |ch| TABLE_FOR_ESCAPE_HTML__[ch] end end end class String def ord self.bytes[0] end end class Chalk COMMENT_START_CHARS = { ruby: /#./, cpp: /\/\*|\/\//, c: /\/\// } COMMENT_END_CHARS = { cpp: /\*\/|.\n/, ruby: /.\n/, c: /.\n/, } STRING_SEP = %w(' ") SEPARATORS = " @(){}[],.:;\"\'`<>=+-*/\t\n\\?|&#" SEPARATORS_RX = /[@\(\)\{\}\[\],\.\:;"'`\<\>=\+\-\*\/\t\n\\\?\|\&#]/ def initialize(file) @filename = file @file = File.new(file) @rnd = Random.new(file.hash) @tokens = {} reset end def parse &block reset() @file.read.each_char do |char| @last_couple = ((@last_couple.size < 2) ? @last_couple : @last_couple[1]) + char case(@state) when :source if start_comment?(@last_couple) @state = :comment elsif STRING_SEP.include?(char) @string_started_with = char @state = :string else process_entity(&block) if (@entity.length == 1 && SEPARATORS.index(@entity)) || SEPARATORS.index(char) end when :comment process_entity(:source, &block) if end_comment?(@last_couple) when :string if (STRING_SEP.include?(char) && @string_started_with == char) @entity += char process_entity(:source, &block) char = '' elsif char == '\\' @state = :escaped_char else end when :escaped_char @state = :string end @entity += char end end def to_html(&block) html = '' if block block.call( '<table><tr><td><pre>' ) else html = '<table><tr><td><pre>' end line_n = 1 @file.readlines.each do if block block.call( "<a href='#'><b>#{line_n}</b></a>\n" ) else html += "<a href='#'><b>#{line_n}</b></a>\n" end line_n += 1 end @file = File.open(@filename) if block block.call( '</pre></td><td><pre>' ) else html += '</pre></td><td><pre>' end parse do |entity, type| entity = entity.gsub("\t", ' ') if block block.call( entity ) #block.call(highlight( entity , type)) else html += entity #html += highlight( entity , type) end end if block block.call( '</pre><td></tr></table>' ) else html + '</pre><td></tr></table>' end end def language @language ||= case(@file.path.to_s.split('.').last.to_sym) when :rb :ruby when :cpp, :hpp :cpp when :c, :h :c when :py :python else @file.path.to_s.split('.').last.to_s end end private def process_entity(new_state = nil, &block) block.call @entity, @state if block @entity = '' @state = new_state if new_state end def reset @file = File.open(@filename) if @file @state = :source @string_started_with = '' @entity = '' @last_couple = '' end def color(entity) entity = entity.strip entity.gsub! SEPARATORS_RX, '' token = '' return token if entity.empty? #return token if token = @tokens[entity] return '' if entity[0].ord >= 128 rgb = [ @rnd.rand(150) + 100, @rnd.rand(150) + 100, @rnd.rand(150) + 100 ] token = String.sprintf("#%02X%02X%02X", rgb[0], rgb[1], rgb[2]) #token = "#%02X%02X%02X" % rgb #@tokens[entity] = token return token end def highlight(entity, type) esc_entity = CGI.escapeHTML( entity ) case type when :string, :comment "<span class='#{type}'>#{esc_entity}</span>" else rgb = color(entity) if rgb.empty? esc_entity else "<span rel='t#{rgb.hash}' style='color: #{rgb}' >#{esc_entity}</span>" end end end def start_comment?(char) rx = COMMENT_START_CHARS[language] char.match rx if rx end def end_comment?(char) rx = COMMENT_END_CHARS[language] char.match rx if rx end end 


And the actual code that reads the file and numbering:
 r = Nginx::Request.new Nginx.rputs '<html><link rel="stylesheet" href="/st/code.css" type="text/css" /><body>' begin ch = Chalk.new(r.filename) data = ch.to_html Nginx.rputs data rescue => e Nginx.rputs e.message end Nginx.rputs '</body></html>' 

We connect everything. Since class Chalk is used constantly, we will load it in advance:
mruby_init '/opt/app/init.rb';
This line is added before the server section in the settings. Next, we specify our handler:
  location ~ \.rb$ { add_header Content-Type text/html; mruby_content_handler "/opt/app/parse_code.rb" cache; } 

Everything, now it is possible to look at result: mruby.ajieks.ru/sinatra/lib/sinatra/main.rb

Conclusion


Thus, it is possible to implement advanced query processing, filtering, and caching using another one of the languages. Whether this module is ready for use in combat conditions, I do not know. While testing, there have been hangs of the entire server, but there is a probability of curvature of the hands, or, nevertheless, not everything has been completely modified. I will follow the development of the project.

Those interested can drive on the performance of the scripts in the article on the links above.
The server is deployed on DigitalOcean on the simplest machine, Ubuntu 12.04 x64. Number of processes 2, connections 1024. No additional settings were made. In case of server hangup, I restarted nginx every 10 minutes.

Source: https://habr.com/ru/post/225313/


All Articles