Quick start on writing a module?

I’d like to convert an apache module I’ve written to work
under nginx. Can somebody loan me a clue for getting started?

Background

The logic is pretty simple:

  1. based on the input URI, determine what file will be
    sent. this will possibly involved a (cached) db lookup.

2a. set these headers:

     Content-disposition:
     ETag:
     X-mod_asset-version: (my custom header)

2b. set mime type.

  1. allow nginx to send the bytes of the file. Hopefully
    it will handle these headers:

    Last-Modified:
    Content-Length:
    Accept-Ranges:
    Content-Range:
    Content-Length:

Questions

For 2b, this is the proper way?

 r->headers_out.content_type.len = sizeof("image/gif") - 1;
 r->headers_out.content_type.data = (u_char *) "image/gif";

 Is there a special way to malloc the .data so that it
 will be freed automatically when the request is complete?

For 3, should I be using X-Accel-Redirect?

 add_header("X-Accel-Redirect: /protected/iso.img");

If so, then I can simply set my headers and pass along to the next
phase?

Any simple example modules I can use as a starting point?

Many TIA!!
Mark


Mark H.
Pixar Animation Studios

Hello!

On Wed, Jun 16, 2010 at 02:29:43AM -0700, Mark H. wrote:

I’d like to convert an apache module I’ve written to work
under nginx. Can somebody loan me a clue for getting started?

It’s probably good idea to start from here:

http://nginx.org/en/links.html

http://wiki.nginx.org/NginxResources#Developer_resources

Note well: in most cases it’s good idea to rewrite module from
scratch instead of trying to adopt something written for Apache.

    Content-disposition:
    Accept-Ranges:
    Content-Range:
    Content-Length:

Problematic part is “db lookup”. In nginx you have to do it in
non-blocking way, and this isn’t something readily available in
database client libraries. You may want to deligate this part to
some backend or already available modules like ngx_postgres
(NGINX 3rd Party Modules | NGINX).

Questions

For 2b, this is the proper way?

r->headers_out.content_type.len = sizeof("image/gif") - 1;
r->headers_out.content_type.data = (u_char *) "image/gif";

No.

For 0.7.* use:

r->headers_out.content_type_len = sizeof("image/gif") - 1;
r->headers_out.content_type.len = sizeof("image/gif") - 1;
r->headers_out.content_type.data = (u_char *) "image/gif";

For 0.8.37+ use ngx_str_set() macro to set string, i.e.

r->headers_out.content_type_len = sizeof("image/gif") - 1;
ngx_str_set(&r->headers_out.content_type, "image/gif");
Is there a special way to malloc the .data so that it
will be freed automatically when the request is complete?

In the above case you don’t need to allocate anything as it uses
constants strings. For dynamic allocations use
ngx_pool_alloc(r->pool).

For 3, should I be using X-Accel-Redirect?

add_header("X-Accel-Redirect: /protected/iso.img");

If so, then I can simply set my headers and pass along to the next
phase?

No. X-Accel-Redirect is special header understood by upstream
module (proxy, fastcgi) in backend responses. In your own module
you have to use ngx_http_internal_redirect() function for this.

Any simple example modules I can use as a starting point?

For simple content generation see ngx_http_empty_gif_module.c,
ngx_http_static_module.c, ngx_http_flv_module.c.

For simple examples of internal redirect usage see
ngx_http_index_module.c, ngx_http_random_index_module.c.

Maxim D.

Hi,
Maxim already answered most of your questions regarding writing a module
like this from scratch. I would like to add that you can already do
this,
without writing new module, by misusing Maxim’s excellent
ngx_http_auth_request_module [1] and ngx_postgres [2].

There are two “but”:

  1. Required “postgres_set” directive is still available only in my local
    repository. I intend to push it to GitHub today or tomorrow, after
    adding
    some test cases, so stay tuned :slight_smile:
  2. You need to use “proxy_pass” for now. Ideally, you should create your
    own
    mini-module that would serve static files based on complex_value
    ($location). This should be mostly copy & paste of
    “ngx_http_static_handler”
    from nginx’s ngx_http_static_module.c.

[1] ngx_http_auth_request_module: log
[2] FRiCKLE Labs / nginx / ngx_postgres

Best regards,
Piotr S. < [email protected] >

— configuration —
upstream pgsql {
postgres_server 127.0.0.1 dbname=test user=test password=test;
}

server {
listen 80;

location = /auth {
internal;
postgres_pass pgsql;
postgres_query “SELECT ‘c-d’, ‘123456’, ‘0.1’, ‘/etc/passwd’”;
postgres_set $pg_disposition 0 0 required;
postgres_set $pg_etag 0 1 required;
postgres_set $pg_version 0 2 required;
postgres_set $pg_location 0 3 required;
}

location / {
auth_request /auth;
auth_request_set $disposition $pg_disposition;
auth_request_set $etag $pg_etag;
auth_request_set $version $pg_version;
auth_request_set $location $pg_location;
add_header “Content-Disposition” $disposition;
add_header “Etag” $etag;
add_header “X-mod_asset-version” $version;
proxy_pass http://127.0.0.1:81/$location;
}
}

proxy for serving static files

server {
listen 81;

location / {
root /;
}
}

— output —
$ curl -v http://localhost/

  • About to connect() to localhost port 80 (#0)
  • Trying 127.0.0.1… connected
  • Connected to localhost (127.0.0.1) port 80 (#0)

GET / HTTP/1.1
User-Agent: curl/7.19.5
Host: localhost
Accept: /

< HTTP/1.1 200 OK
< Server: nginx/0.8.41
< Date: Wed, 16 Jun 2010 16:55:30 GMT
< Content-Type: text/plain
< Connection: keep-alive
< Content-Length: 3078
< Last-Modified: Sat, 15 May 2010 01:27:40 GMT
< Accept-Ranges: bytes
< Content-Disposition: c-d
< Etag: 123456
< X-mod_asset-version: 0.1
<
root:*:0:0:Charlie &:/root:/bin/ksh
(…)

Hello!

On Wed, Jun 16, 2010 at 05:13:40PM +0200, Piotr S. wrote:

Hi,
Maxim already answered most of your questions regarding writing a
module like this from scratch. I would like to add that you can
already do this, without writing new module, by misusing Maxim’s
excellent ngx_http_auth_request_module [1] and ngx_postgres [2].

I believe Valery K.'s eval module
(http://grid.net.ru/nginx/eval.en.html) would fit better here than
auth request module.

There are two “but”:

  1. Required “postgres_set” directive is still available only in my
    local repository. I intend to push it to GitHub today or tomorrow,
    after adding some test cases, so stay tuned :slight_smile:

I believe this may be handled by eval as long as postgres module
is able to return something it understands.

  1. You need to use “proxy_pass” for now. Ideally, you should create
    your own mini-module that would serve static files based on
    complex_value ($location). This should be mostly copy & paste of
    “ngx_http_static_handler” from nginx’s ngx_http_static_module.c.

Something like alias /path/to/$location; should do the trick.

Also, with eval it’s possible to use regular rewrites instead.

Maxim D.

On Wed, Jun 16, 2010 at 5:29 PM, Mark H. [email protected] wrote:

  1. based on the input URI, determine what file will be
    sent. this will possibly involved a (cached) db lookup.

Well, (cached) db lookup can be done via ngx_drizzle/ngx_postgres +
ngx_srcache + ngx_memc + memcached :slight_smile:

2a. set these headers:

   Content-disposition:
   ETag:
   X-mod_asset-version: (my custom header)

ngx_headers and/or ngx_headers_more may just do the work or at least
can serve as good examples.

Cheers,
-agentzh

Hi,

I believe Valery K.'s eval module
(http://grid.net.ru/nginx/eval.en.html) would fit better here than
auth request module.
(…)
I believe this may be handled by eval as long as postgres module
is able to return something it understands.

I can’t agree with you here. ngx_postgres can either return single value
in
clear text or whole result-set in RDS format (which can be converted to
JSON
using ngx_rds_json [1]). Since we need more than one value, we would
need to
either do multiple queries for each value or use JSON. Of course, we
could
extract values from JSON using ngx_lua [2], but this seems like quite an
overhead for something that could be easily achieved by misusing your
module.

Something like alias /path/to/$location; should do the trick.

I tried that before suggesting “proxy_pass”.

Unfortunately both “alias $location;” and “alias /$location;” results in
“/etc/passwdindex.html” is not a directory while sending to client".

[1] GitHub - openresty/rds-json-nginx-module: An nginx output filter that formats Resty DBD Streams generated by ngx_drizzle and others to JSON
[2] GitHub - openresty/lua-nginx-module: Embed the Power of Lua into NGINX HTTP servers

Best regards,
Piotr S. < [email protected] >

Hello!

On Wed, Jun 16, 2010 at 05:46:32PM +0200, Piotr S. wrote:

value in clear text or whole result-set in RDS format (which can be
converted to JSON using ngx_rds_json [1]). Since we need more than
one value, we would need to either do multiple queries for each
value or use JSON. Of course, we could extract values from JSON
using ngx_lua [2], but this seems like quite an overhead for
something that could be easily achieved by misusing your module.

I wrote “as long as …” on purpose, as I have no idea what
postrgess module is able to produce, sorry.

But as long as it’s able to return single value in clear text it
should be possible to write select which will return all needed
data separated by newlines. This should be understood by eval
module easily.

Something like alias /path/to/$location; should do the trick.

I tried that before suggesting “proxy_pass”.

Unfortunately both “alias $location;” and “alias /$location;”
results in “/etc/passwdindex.html” is not a directory while sending
to client".

This is caused by index module and only applies when uri ends with
‘/’. Workarounds are obvious, but I doubt they are actually
needed in this particular case.

Maxim D.

Oops. You’re right. I didn’t notice that eval module allowed multiple
variables until now. In that case, it might be indeed a better fit than
ngx_http_auth_request_module.

…or not. It seems that eval module evaluates multiple variables only
for
“application/x-www-form-urlencoded” content, so it won’t work with clear
text values separated by new lines.

Best regards,
Piotr S. < [email protected] >

Hello!

On Wed, Jun 16, 2010 at 06:47:29PM +0200, Piotr S. wrote:

[…]

}

“GET /aaa” results in:

[…]

*4 open() “/etc/passwdaaa” failed (2: No such file or directory)"

No, it’s not bug, it works as documented. Alias specifies full
path only in regexp locations (with captures). For normal locations
it specifies path to point matched by location (even if alias
contains variables).

Maxim D.

Hi,

I wrote “as long as …” on purpose, as I have no idea what
postrgess module is able to produce, sorry.

But as long as it’s able to return single value in clear text it
should be possible to write select which will return all needed
data separated by newlines. This should be understood by eval
module easily.

Oops. You’re right. I didn’t notice that eval module allowed multiple
variables until now. In that case, it might be indeed a better fit than
ngx_http_auth_request_module.

One note though, you will need to use agentzh’s fork of the eval module
[1],
because ngx_postgres currently doesn’t work with subrequests in memory.

This is caused by index module and only applies when uri ends with
‘/’. Workarounds are obvious, but I doubt they are actually
needed in this particular case.

Yes, my previous error message was produced by index module,
but the issue exists and should be probably considered a bug:

location / {
set $file “/etc/passwd”;
alias $file;
}

“GET /aaa” results in:

*4 http script var: “/etc/passwd”
*4 http filename: “/etc/passwdaaa”
*4 add cleanup: 000000020E509C88
*4 open() “/etc/passwdaaa” failed (2: No such file or directory)"
*4 http finalize request: 404, “/aaa?” a:1, c:1

This can indeed be worked-around by using:

location ~ (/.*) {
set $file “/etc/passwd”;
alias $file;
}

which results in:

*2 http script var: “/etc/passwd”
*2 http filename: “/etc/passwd”
*2 add cleanup: 00000002097A0CA0
*2 http static fd: 12

From what I can tell “ngx_http_map_uri_to_path()” always adds URI to the
path, unless you’re using captures in the location.

[1] GitHub - openresty/nginx-eval-module: A module for evaluating memcached or proxy response into variable

Best regards,
Piotr S. < [email protected] >

Hi,

No, it’s not bug, it works as documented. Alias specifies full
path only in regexp locations (with captures). For normal locations
it specifies path to point matched by location (even if alias
contains variables).

Uhm… I stand corrected. Russian documentation indeed says that.

Thanks!

Best regards,
Piotr S. < [email protected] >