about summary refs log tree commit diff
diff options
context:
space:
mode:
authorLaurent Bercot <ska-skaware@skarnet.org>2024-03-06 12:29:36 +0000
committerLaurent Bercot <ska@appnovation.com>2024-03-06 12:29:36 +0000
commit4a2abe7bf4c05db91093e4a2d29d27430316bc05 (patch)
treef2ef6b5b77d4ac0dfef4f317b0a520bba1d4e2e1
parent628b64629d17fef775838736a17ff43bcef8e471 (diff)
downloadtipidee-4a2abe7bf4c05db91093e4a2d29d27430316bc05.tar.gz
tipidee-4a2abe7bf4c05db91093e4a2d29d27430316bc05.tar.xz
tipidee-4a2abe7bf4c05db91093e4a2d29d27430316bc05.zip
Add ls.cgi, prepare for 0.0.4.0
Signed-off-by: Laurent Bercot <ska@appnovation.com>
-rw-r--r--.gitignore1
-rw-r--r--INSTALL4
-rw-r--r--NEWS14
-rw-r--r--doc/index.html7
-rw-r--r--doc/ls.cgi.html64
-rw-r--r--doc/upgrade.html14
-rw-r--r--package/deps.mak10
-rw-r--r--package/info2
-rw-r--r--package/targets.mak3
-rw-r--r--src/include/tipidee/util.h2
-rw-r--r--src/libtipidee/deps-lib/tipidee1
-rw-r--r--src/libtipidee/tipidee_util_htmlescape.c38
-rw-r--r--src/misc/deps-exe/ls.cgi2
-rw-r--r--src/misc/ls.cgi.c154
14 files changed, 304 insertions, 12 deletions
diff --git a/.gitignore b/.gitignore
index b0fadd7..111064b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@
 /tipideed
 /tipidee-config
 /tipidee-config-preprocess
+/ls.cgi
diff --git a/INSTALL b/INSTALL
index e1ca77e..878170f 100644
--- a/INSTALL
+++ b/INSTALL
@@ -6,8 +6,8 @@ Build Instructions
 
   - A POSIX-compliant C development environment
   - GNU make version 3.81 or later
-  - skalibs version 2.14.1.0 or later: https://skarnet.org/software/skalibs/
-  - (optional but recommended): s6-networking version 2.7.0.1 or later:
+  - skalibs version 2.14.1.1 or later: https://skarnet.org/software/skalibs/
+  - (optional but recommended): s6-networking version 2.7.0.2 or later:
       https://skarnet.org/software/s6-networking/
 
  This software will run on any operating system that implements
diff --git a/NEWS b/NEWS
index c37343d..b933658 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,19 @@
 Changelog for tipidee.
 
+In 0.0.4.0
+----------
+
+ - Bugfixes.
+ - New ls.cgi binary for use in document hierarchies.
+
+
+In 0.0.3.0
+----------
+
+ - Bugfixes.
+ - New XXX_no_translate configuration option.
+
+
 In 0.0.2.0
 ----------
 
diff --git a/doc/index.html b/doc/index.html
index 2aef76b..0557d6f 100644
--- a/doc/index.html
+++ b/doc/index.html
@@ -111,11 +111,11 @@ make it shorter. Just like the code.
  <li> A POSIX-compliant system with a standard C development environment </li>
  <li> GNU make, version 3.81 or later </li>
  <li> <a href="//skarnet.org/software/skalibs/">skalibs</a> version
-2.14.1.0 or later. It's a build-time requirement. It's also a run-time
+2.14.1.1 or later. It's a build-time requirement. It's also a run-time
 requirement if you link against the shared version of the skalibs
 library. </li>
  <li> Recommended at run-time: <a href="//skarnet.org/software/s6-networking/">s6-networking</a> version
-2.7.0.1 or later. It's not a strict requirement, but tipidee relies on a super-server such as
+2.7.0.2 or later. It's not a strict requirement, but tipidee relies on a super-server such as
 <a href="//skarnet.org/software/s6-networking/s6-tcpserver.html">s6-tcpserver</a>
 to listen to the network and provide connection
 information via environment variables. It also defers to tools such as
@@ -143,7 +143,7 @@ Don't take my word for it; try it out for yourself. </li>
 
 <ul>
  <li> The current released version of tipidee is
-<a href="tipidee-0.0.3.0.tar.gz">0.0.3.0</a>. </li>
+<a href="tipidee-0.0.4.0.tar.gz">0.0.4.0</a>. </li>
  <li> You can checkout a copy of the
 <a href="//git.skarnet.org/cgi-bin/cgit.cgi/tipidee/">tipidee
 git repository</a>:
@@ -181,6 +181,7 @@ the previous versions of tipidee and the current one. </li>
 
 <ul>
 <li><a href="tipidee-config-preprocess.html">The <tt>tipidee-config-preprocess</tt> internal program</a></li>
+<li><a href="ls.cgi.html">The <tt>ls.cgi</tt> internal program</a></li>
 </ul>
 
 <h3> Configuration format </h3>
diff --git a/doc/ls.cgi.html b/doc/ls.cgi.html
new file mode 100644
index 0000000..c864dfb
--- /dev/null
+++ b/doc/ls.cgi.html
@@ -0,0 +1,64 @@
+<html>
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta http-equiv="Content-Language" content="en" />
+    <title>tipidee: the ls.cgi program</title>
+    <meta name="Description" content="tipidee: the ls.cgi internal program" />
+    <meta name="Keywords" content="tipidee ls.cgi index index.cgi listing directory cgi http" />
+    <!-- <link rel="stylesheet" type="text/css" href="//skarnet.org/default.css" /> -->
+  </head>
+<body>
+
+<p>
+<a href="index.html">tipidee</a><br />
+<a href="//skarnet.org/software/">Software</a><br />
+<a href="//skarnet.org/">skarnet.org</a>
+</p>
+
+<h1> The <tt>ls.cgi</tt> internal program </h1>
+
+<p>
+ <tt>ls.cgi</tt> is a binary that is suitable for using as a CGI script,
+e.g. called <tt>index.cgi</tt>,
+in a Web document hierarchy served by tipidee, when the goal of the index
+file is only to list the contents of the directory it resides in.
+</p>
+
+<h2> Interface </h2>
+
+<pre>
+     ls.cgi
+</pre>
+
+<ul>
+ <li> It is not meant to be invoked by users. It will fail if invoked outside of a
+CGI script context. As such, it is unexported (it will appear in <tt>libexecdir</tt>,
+not in <tt>bindir</tt>). </li>
+ <li> It is meant to be placed in the repository of CGI scripts and symlinked as
+<tt>index.cgi</tt> in the places where the URL of a directory should list the
+contents of said directory. </li>
+ <li> For instance, if <tt>example.com:443/index.cgi</tt> is a copy of, or a
+symlink to, ls.cgi, and the tipidee configuration says that this binary is meant
+to be a CGI script, then access to <tt>https://example.com</tt> will list the files
+available in the <tt>example.com:443/</tt> directory. </li>
+</ul>
+
+<h2> Exit codes </h2>
+
+<dl>
+ <dt> 0 </dt> <dd> success </dd>
+ <dt> 100 </dt> <dd> wrong usage </dd>
+ <dt> 111 </dt> <dd> system call failed </dd>
+</dl>
+
+<h2> Notes </h2>
+
+<ul>
+ <li> ls.cgi is very minimalistic. It provides no CSS or anything else than
+the bare list of files. It is provided as an example of how CGI scripts can
+work with tipidee. </li>
+</ul>
+
+</body>
+</html>
diff --git a/doc/upgrade.html b/doc/upgrade.html
index f2a6880..b7a7d29 100644
--- a/doc/upgrade.html
+++ b/doc/upgrade.html
@@ -18,7 +18,17 @@
 
 <h1> What has changed in tipidee </h1>
 
-<h2> in.0.0.3.0 </h2>
+<h2> in 0.0.4.0 </h2>
+
+<ul>
+ <li> <a href="//skarnet.org/software/skalibs/">skalibs</a>
+dependency bumped to 2.14.1.1 </li>
+ <li> <a href="//skarnet.org/software/s6-networking/">s6-networking</a>
+recommendation bumped to 2.7.0.2 </li>
+ <li> New <a href="ls.cgi.html">ls.cgi</a> binary </li>
+</ul>
+
+<h2> in 0.0.3.0 </h2>
 
 <ul>
  <li> <a href="//skarnet.org/software/skalibs/">skalibs</a>
@@ -28,7 +38,7 @@ recommendation bumped to 2.7.0.1 </li>
  <li> New <tt>global XXX_no_translate</tt> configuration directive. </li>
 </ul>
 
-<h2> in.0.0.2.0 </h2>
+<h2> in 0.0.2.0 </h2>
 
 <ul>
  <li> <a href="//skarnet.org/software/skalibs/">skalibs</a>
diff --git a/package/deps.mak b/package/deps.mak
index b5296c5..7085bcb 100644
--- a/package/deps.mak
+++ b/package/deps.mak
@@ -62,8 +62,10 @@ src/libtipidee/tipidee_rql_read.o src/libtipidee/tipidee_rql_read.lo: src/libtip
 src/libtipidee/tipidee_uri_parse.o src/libtipidee/tipidee_uri_parse.lo: src/libtipidee/tipidee_uri_parse.c src/include/tipidee/uri.h
 src/libtipidee/tipidee_util_chunked_read.o src/libtipidee/tipidee_util_chunked_read.lo: src/libtipidee/tipidee_util_chunked_read.c src/include/tipidee/util.h
 src/libtipidee/tipidee_util_defaulttext.o src/libtipidee/tipidee_util_defaulttext.lo: src/libtipidee/tipidee_util_defaulttext.c src/include/tipidee/util.h
+src/libtipidee/tipidee_util_htmlescape.o src/libtipidee/tipidee_util_htmlescape.lo: src/libtipidee/tipidee_util_htmlescape.c src/include/tipidee/util.h
 src/libtipidee/tipidee_util_httpdate.o src/libtipidee/tipidee_util_httpdate.lo: src/libtipidee/tipidee_util_httpdate.c src/include/tipidee/util.h
 src/libtipidee/tipidee_util_parse_range.o src/libtipidee/tipidee_util_parse_range.lo: src/libtipidee/tipidee_util_parse_range.c src/include/tipidee/util.h
+src/misc/ls.cgi.o src/misc/ls.cgi.lo: src/misc/ls.cgi.c src/include/tipidee/response.h src/include/tipidee/util.h
 src/tipideed/cgi.o src/tipideed/cgi.lo: src/tipideed/cgi.c src/include/tipidee/tipidee.h src/tipideed/tipideed-internal.h
 src/tipideed/errors.o src/tipideed/errors.lo: src/tipideed/errors.c src/include/tipidee/log.h src/include/tipidee/response.h src/include/tipidee/util.h src/tipideed/tipideed-internal.h
 src/tipideed/harden.o src/tipideed/harden.lo: src/tipideed/harden.c src/tipideed/tipideed-internal.h
@@ -80,12 +82,14 @@ tipidee-config: src/config/tipidee-config.o src/config/util.o src/config/node.o
 tipidee-config-preprocess: EXTRA_LIBS := -lskarnet
 tipidee-config-preprocess: src/config/tipidee-config-preprocess.o
 ifeq ($(strip $(STATIC_LIBS_ARE_PIC)),)
-libtipidee.a.xyzzy: src/libtipidee/tipidee_conf_free.o src/libtipidee/tipidee_conf_get.o src/libtipidee/tipidee_conf_get_argv.o src/libtipidee/tipidee_conf_get_content_type.o src/libtipidee/tipidee_conf_get_errorfile.o src/libtipidee/tipidee_conf_get_redirection.o src/libtipidee/tipidee_conf_get_resattr.o src/libtipidee/tipidee_conf_get_resattr1.o src/libtipidee/tipidee_conf_get_responseheaders.o src/libtipidee/tipidee_conf_get_string.o src/libtipidee/tipidee_conf_get_uint32.o src/libtipidee/tipidee_conf_init.o src/libtipidee/tipidee_headers_get_content_length.o src/libtipidee/tipidee_headers_init.o src/libtipidee/tipidee_headers_parse.o src/libtipidee/tipidee_headers_search.o src/libtipidee/tipidee_log_answer.o src/libtipidee/tipidee_log_exit.o src/libtipidee/tipidee_log_request.o src/libtipidee/tipidee_log_resource.o src/libtipidee/tipidee_log_start.o src/libtipidee/tipidee_method.o src/libtipidee/tipidee_response_error_nofile.o src/libtipidee/tipidee_response_error_nofile_G.o src/libtipidee/tipidee_response_file.o src/libtipidee/tipidee_response_file_G.o src/libtipidee/tipidee_response_header_date.o src/libtipidee/tipidee_response_header_date_G.o src/libtipidee/tipidee_response_header_date_fmt.o src/libtipidee/tipidee_response_header_end.o src/libtipidee/tipidee_response_header_lastmodified.o src/libtipidee/tipidee_response_header_preparebuiltin.o src/libtipidee/tipidee_response_header_write.o src/libtipidee/tipidee_response_header_writeall.o src/libtipidee/tipidee_response_header_writeall_G.o src/libtipidee/tipidee_response_header_writemerge.o src/libtipidee/tipidee_response_header_writemerge_G.o src/libtipidee/tipidee_response_partial.o src/libtipidee/tipidee_response_partial_G.o src/libtipidee/tipidee_response_status.o src/libtipidee/tipidee_rql_read.o src/libtipidee/tipidee_uri_parse.o src/libtipidee/tipidee_util_chunked_read.o src/libtipidee/tipidee_util_defaulttext.o src/libtipidee/tipidee_util_httpdate.o src/libtipidee/tipidee_util_parse_range.o
+libtipidee.a.xyzzy: src/libtipidee/tipidee_conf_free.o src/libtipidee/tipidee_conf_get.o src/libtipidee/tipidee_conf_get_argv.o src/libtipidee/tipidee_conf_get_content_type.o src/libtipidee/tipidee_conf_get_errorfile.o src/libtipidee/tipidee_conf_get_redirection.o src/libtipidee/tipidee_conf_get_resattr.o src/libtipidee/tipidee_conf_get_resattr1.o src/libtipidee/tipidee_conf_get_responseheaders.o src/libtipidee/tipidee_conf_get_string.o src/libtipidee/tipidee_conf_get_uint32.o src/libtipidee/tipidee_conf_init.o src/libtipidee/tipidee_headers_get_content_length.o src/libtipidee/tipidee_headers_init.o src/libtipidee/tipidee_headers_parse.o src/libtipidee/tipidee_headers_search.o src/libtipidee/tipidee_log_answer.o src/libtipidee/tipidee_log_exit.o src/libtipidee/tipidee_log_request.o src/libtipidee/tipidee_log_resource.o src/libtipidee/tipidee_log_start.o src/libtipidee/tipidee_method.o src/libtipidee/tipidee_response_error_nofile.o src/libtipidee/tipidee_response_error_nofile_G.o src/libtipidee/tipidee_response_file.o src/libtipidee/tipidee_response_file_G.o src/libtipidee/tipidee_response_header_date.o src/libtipidee/tipidee_response_header_date_G.o src/libtipidee/tipidee_response_header_date_fmt.o src/libtipidee/tipidee_response_header_end.o src/libtipidee/tipidee_response_header_lastmodified.o src/libtipidee/tipidee_response_header_preparebuiltin.o src/libtipidee/tipidee_response_header_write.o src/libtipidee/tipidee_response_header_writeall.o src/libtipidee/tipidee_response_header_writeall_G.o src/libtipidee/tipidee_response_header_writemerge.o src/libtipidee/tipidee_response_header_writemerge_G.o src/libtipidee/tipidee_response_partial.o src/libtipidee/tipidee_response_partial_G.o src/libtipidee/tipidee_response_status.o src/libtipidee/tipidee_rql_read.o src/libtipidee/tipidee_uri_parse.o src/libtipidee/tipidee_util_chunked_read.o src/libtipidee/tipidee_util_defaulttext.o src/libtipidee/tipidee_util_htmlescape.o src/libtipidee/tipidee_util_httpdate.o src/libtipidee/tipidee_util_parse_range.o
 else
-libtipidee.a.xyzzy: src/libtipidee/tipidee_conf_free.lo src/libtipidee/tipidee_conf_get.lo src/libtipidee/tipidee_conf_get_argv.lo src/libtipidee/tipidee_conf_get_content_type.lo src/libtipidee/tipidee_conf_get_errorfile.lo src/libtipidee/tipidee_conf_get_redirection.lo src/libtipidee/tipidee_conf_get_resattr.lo src/libtipidee/tipidee_conf_get_resattr1.lo src/libtipidee/tipidee_conf_get_responseheaders.lo src/libtipidee/tipidee_conf_get_string.lo src/libtipidee/tipidee_conf_get_uint32.lo src/libtipidee/tipidee_conf_init.lo src/libtipidee/tipidee_headers_get_content_length.lo src/libtipidee/tipidee_headers_init.lo src/libtipidee/tipidee_headers_parse.lo src/libtipidee/tipidee_headers_search.lo src/libtipidee/tipidee_log_answer.lo src/libtipidee/tipidee_log_exit.lo src/libtipidee/tipidee_log_request.lo src/libtipidee/tipidee_log_resource.lo src/libtipidee/tipidee_log_start.lo src/libtipidee/tipidee_method.lo src/libtipidee/tipidee_response_error_nofile.lo src/libtipidee/tipidee_response_error_nofile_G.lo src/libtipidee/tipidee_response_file.lo src/libtipidee/tipidee_response_file_G.lo src/libtipidee/tipidee_response_header_date.lo src/libtipidee/tipidee_response_header_date_G.lo src/libtipidee/tipidee_response_header_date_fmt.lo src/libtipidee/tipidee_response_header_end.lo src/libtipidee/tipidee_response_header_lastmodified.lo src/libtipidee/tipidee_response_header_preparebuiltin.lo src/libtipidee/tipidee_response_header_write.lo src/libtipidee/tipidee_response_header_writeall.lo src/libtipidee/tipidee_response_header_writeall_G.lo src/libtipidee/tipidee_response_header_writemerge.lo src/libtipidee/tipidee_response_header_writemerge_G.lo src/libtipidee/tipidee_response_partial.lo src/libtipidee/tipidee_response_partial_G.lo src/libtipidee/tipidee_response_status.lo src/libtipidee/tipidee_rql_read.lo src/libtipidee/tipidee_uri_parse.lo src/libtipidee/tipidee_util_chunked_read.lo src/libtipidee/tipidee_util_defaulttext.lo src/libtipidee/tipidee_util_httpdate.lo src/libtipidee/tipidee_util_parse_range.lo
+libtipidee.a.xyzzy: src/libtipidee/tipidee_conf_free.lo src/libtipidee/tipidee_conf_get.lo src/libtipidee/tipidee_conf_get_argv.lo src/libtipidee/tipidee_conf_get_content_type.lo src/libtipidee/tipidee_conf_get_errorfile.lo src/libtipidee/tipidee_conf_get_redirection.lo src/libtipidee/tipidee_conf_get_resattr.lo src/libtipidee/tipidee_conf_get_resattr1.lo src/libtipidee/tipidee_conf_get_responseheaders.lo src/libtipidee/tipidee_conf_get_string.lo src/libtipidee/tipidee_conf_get_uint32.lo src/libtipidee/tipidee_conf_init.lo src/libtipidee/tipidee_headers_get_content_length.lo src/libtipidee/tipidee_headers_init.lo src/libtipidee/tipidee_headers_parse.lo src/libtipidee/tipidee_headers_search.lo src/libtipidee/tipidee_log_answer.lo src/libtipidee/tipidee_log_exit.lo src/libtipidee/tipidee_log_request.lo src/libtipidee/tipidee_log_resource.lo src/libtipidee/tipidee_log_start.lo src/libtipidee/tipidee_method.lo src/libtipidee/tipidee_response_error_nofile.lo src/libtipidee/tipidee_response_error_nofile_G.lo src/libtipidee/tipidee_response_file.lo src/libtipidee/tipidee_response_file_G.lo src/libtipidee/tipidee_response_header_date.lo src/libtipidee/tipidee_response_header_date_G.lo src/libtipidee/tipidee_response_header_date_fmt.lo src/libtipidee/tipidee_response_header_end.lo src/libtipidee/tipidee_response_header_lastmodified.lo src/libtipidee/tipidee_response_header_preparebuiltin.lo src/libtipidee/tipidee_response_header_write.lo src/libtipidee/tipidee_response_header_writeall.lo src/libtipidee/tipidee_response_header_writeall_G.lo src/libtipidee/tipidee_response_header_writemerge.lo src/libtipidee/tipidee_response_header_writemerge_G.lo src/libtipidee/tipidee_response_partial.lo src/libtipidee/tipidee_response_partial_G.lo src/libtipidee/tipidee_response_status.lo src/libtipidee/tipidee_rql_read.lo src/libtipidee/tipidee_uri_parse.lo src/libtipidee/tipidee_util_chunked_read.lo src/libtipidee/tipidee_util_defaulttext.lo src/libtipidee/tipidee_util_htmlescape.lo src/libtipidee/tipidee_util_httpdate.lo src/libtipidee/tipidee_util_parse_range.lo
 endif
 libtipidee.so.xyzzy: EXTRA_LIBS := -lskarnet
-libtipidee.so.xyzzy: src/libtipidee/tipidee_conf_free.lo src/libtipidee/tipidee_conf_get.lo src/libtipidee/tipidee_conf_get_argv.lo src/libtipidee/tipidee_conf_get_content_type.lo src/libtipidee/tipidee_conf_get_errorfile.lo src/libtipidee/tipidee_conf_get_redirection.lo src/libtipidee/tipidee_conf_get_resattr.lo src/libtipidee/tipidee_conf_get_resattr1.lo src/libtipidee/tipidee_conf_get_responseheaders.lo src/libtipidee/tipidee_conf_get_string.lo src/libtipidee/tipidee_conf_get_uint32.lo src/libtipidee/tipidee_conf_init.lo src/libtipidee/tipidee_headers_get_content_length.lo src/libtipidee/tipidee_headers_init.lo src/libtipidee/tipidee_headers_parse.lo src/libtipidee/tipidee_headers_search.lo src/libtipidee/tipidee_log_answer.lo src/libtipidee/tipidee_log_exit.lo src/libtipidee/tipidee_log_request.lo src/libtipidee/tipidee_log_resource.lo src/libtipidee/tipidee_log_start.lo src/libtipidee/tipidee_method.lo src/libtipidee/tipidee_response_error_nofile.lo src/libtipidee/tipidee_response_error_nofile_G.lo src/libtipidee/tipidee_response_file.lo src/libtipidee/tipidee_response_file_G.lo src/libtipidee/tipidee_response_header_date.lo src/libtipidee/tipidee_response_header_date_G.lo src/libtipidee/tipidee_response_header_date_fmt.lo src/libtipidee/tipidee_response_header_end.lo src/libtipidee/tipidee_response_header_lastmodified.lo src/libtipidee/tipidee_response_header_preparebuiltin.lo src/libtipidee/tipidee_response_header_write.lo src/libtipidee/tipidee_response_header_writeall.lo src/libtipidee/tipidee_response_header_writeall_G.lo src/libtipidee/tipidee_response_header_writemerge.lo src/libtipidee/tipidee_response_header_writemerge_G.lo src/libtipidee/tipidee_response_partial.lo src/libtipidee/tipidee_response_partial_G.lo src/libtipidee/tipidee_response_status.lo src/libtipidee/tipidee_rql_read.lo src/libtipidee/tipidee_uri_parse.lo src/libtipidee/tipidee_util_chunked_read.lo src/libtipidee/tipidee_util_defaulttext.lo src/libtipidee/tipidee_util_httpdate.lo src/libtipidee/tipidee_util_parse_range.lo
+libtipidee.so.xyzzy: src/libtipidee/tipidee_conf_free.lo src/libtipidee/tipidee_conf_get.lo src/libtipidee/tipidee_conf_get_argv.lo src/libtipidee/tipidee_conf_get_content_type.lo src/libtipidee/tipidee_conf_get_errorfile.lo src/libtipidee/tipidee_conf_get_redirection.lo src/libtipidee/tipidee_conf_get_resattr.lo src/libtipidee/tipidee_conf_get_resattr1.lo src/libtipidee/tipidee_conf_get_responseheaders.lo src/libtipidee/tipidee_conf_get_string.lo src/libtipidee/tipidee_conf_get_uint32.lo src/libtipidee/tipidee_conf_init.lo src/libtipidee/tipidee_headers_get_content_length.lo src/libtipidee/tipidee_headers_init.lo src/libtipidee/tipidee_headers_parse.lo src/libtipidee/tipidee_headers_search.lo src/libtipidee/tipidee_log_answer.lo src/libtipidee/tipidee_log_exit.lo src/libtipidee/tipidee_log_request.lo src/libtipidee/tipidee_log_resource.lo src/libtipidee/tipidee_log_start.lo src/libtipidee/tipidee_method.lo src/libtipidee/tipidee_response_error_nofile.lo src/libtipidee/tipidee_response_error_nofile_G.lo src/libtipidee/tipidee_response_file.lo src/libtipidee/tipidee_response_file_G.lo src/libtipidee/tipidee_response_header_date.lo src/libtipidee/tipidee_response_header_date_G.lo src/libtipidee/tipidee_response_header_date_fmt.lo src/libtipidee/tipidee_response_header_end.lo src/libtipidee/tipidee_response_header_lastmodified.lo src/libtipidee/tipidee_response_header_preparebuiltin.lo src/libtipidee/tipidee_response_header_write.lo src/libtipidee/tipidee_response_header_writeall.lo src/libtipidee/tipidee_response_header_writeall_G.lo src/libtipidee/tipidee_response_header_writemerge.lo src/libtipidee/tipidee_response_header_writemerge_G.lo src/libtipidee/tipidee_response_partial.lo src/libtipidee/tipidee_response_partial_G.lo src/libtipidee/tipidee_response_status.lo src/libtipidee/tipidee_rql_read.lo src/libtipidee/tipidee_uri_parse.lo src/libtipidee/tipidee_util_chunked_read.lo src/libtipidee/tipidee_util_defaulttext.lo src/libtipidee/tipidee_util_htmlescape.lo src/libtipidee/tipidee_util_httpdate.lo src/libtipidee/tipidee_util_parse_range.lo
+ls.cgi: EXTRA_LIBS := -lskarnet
+ls.cgi: src/misc/ls.cgi.o libtipidee.a.xyzzy
 tipideed: EXTRA_LIBS := -lskarnet
 tipideed: src/tipideed/tipideed.o src/tipideed/cgi.o src/tipideed/errors.o src/tipideed/harden.o src/tipideed/options.o src/tipideed/regular.o src/tipideed/redirection.o src/tipideed/send_file.o src/tipideed/trace.o src/tipideed/util.o libtipidee.a.xyzzy
 INTERNAL_LIBS :=
diff --git a/package/info b/package/info
index a525420..0feefa4 100644
--- a/package/info
+++ b/package/info
@@ -1,4 +1,4 @@
 package=tipidee
-version=0.0.3.0
+version=0.0.4.0
 category=web
 package_macro_name=TIPIDEE
diff --git a/package/targets.mak b/package/targets.mak
index 1cf5bca..b1bc92e 100644
--- a/package/targets.mak
+++ b/package/targets.mak
@@ -3,6 +3,7 @@ tipideed \
 tipidee-config
 
 LIBEXEC_TARGETS := \
-tipidee-config-preprocess
+tipidee-config-preprocess \
+ls.cgi
 
 LIB_DEFS := TIPIDEE=tipidee
diff --git a/src/include/tipidee/util.h b/src/include/tipidee/util.h
index 18eb2ce..b3d1151 100644
--- a/src/include/tipidee/util.h
+++ b/src/include/tipidee/util.h
@@ -34,4 +34,6 @@ extern int tipidee_util_httpdate (char const *, tain *) ;
 extern int tipidee_util_defaulttext (unsigned int, tipidee_defaulttext *) ;
 extern int tipidee_util_parse_range (char const *, off_t, uint64_t *, uint64_t *) ;
 
+extern char const *tipidee_util_htmlescape (char const *) ;
+
 #endif
diff --git a/src/libtipidee/deps-lib/tipidee b/src/libtipidee/deps-lib/tipidee
index 33f9f2f..b0b2c08 100644
--- a/src/libtipidee/deps-lib/tipidee
+++ b/src/libtipidee/deps-lib/tipidee
@@ -42,6 +42,7 @@ tipidee_rql_read.o
 tipidee_uri_parse.o
 tipidee_util_chunked_read.o
 tipidee_util_defaulttext.o
+tipidee_util_htmlescape.o
 tipidee_util_httpdate.o
 tipidee_util_parse_range.o
 -lskarnet
diff --git a/src/libtipidee/tipidee_util_htmlescape.c b/src/libtipidee/tipidee_util_htmlescape.c
new file mode 100644
index 0000000..1dfd552
--- /dev/null
+++ b/src/libtipidee/tipidee_util_htmlescape.c
@@ -0,0 +1,38 @@
+/* ISC license. */
+
+#include <stdlib.h>
+
+#include <tipidee/util.h>
+
+struct htmlescape_s
+{
+  char c ;
+  char const *code ;
+} ;
+
+static struct htmlescape_s const table[] =
+{
+  { .c = '\"', .code = "&quot;" },
+  { .c = '&', .code = "&amp;" },
+  { .c = '\'', .code = "&#39;" },
+  { .c = '<', .code = "&lt;" },
+  { .c = '>', .code = "&gt;" },
+} ;
+
+static int htmlescape_cmp (void const *a, void const *b)
+{
+  char aa = *((char const *)a) ;
+  char bb = ((struct htmlescape_s const *)b)->c ;
+  return aa < bb ? -1 : aa > bb ;
+}
+
+char const *tipidee_util_htmlescape (char const *s)
+{
+  struct htmlescape_s const *p = bsearch(
+    s,
+    table,
+    sizeof(table) / sizeof(struct htmlescape_s),
+    sizeof(struct htmlescape_s),
+    &htmlescape_cmp) ;
+  return p ? p->code : s ;
+}
diff --git a/src/misc/deps-exe/ls.cgi b/src/misc/deps-exe/ls.cgi
new file mode 100644
index 0000000..8ecd888
--- /dev/null
+++ b/src/misc/deps-exe/ls.cgi
@@ -0,0 +1,2 @@
+libtipidee.a.xyzzy
+-lskarnet
diff --git a/src/misc/ls.cgi.c b/src/misc/ls.cgi.c
new file mode 100644
index 0000000..d72ae10
--- /dev/null
+++ b/src/misc/ls.cgi.c
@@ -0,0 +1,154 @@
+/* ISC license. */
+
+#include <skalibs/bsdsnowflake.h>
+
+#include <errno.h>
+#include <stdlib.h>
+
+#include <skalibs/gccattributes.h>
+#include <skalibs/buffer.h>
+#include <skalibs/stat.h>
+#include <skalibs/direntry.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/unix-transactional.h>
+
+#include <tipidee/response.h>
+#include <tipidee/util.h>
+
+#define USAGE "ls.cgi is meant to be used as a CGI script invoked by tipideed"
+#define dieusage() strerr_dieusage(100, USAGE)
+#define dienomem() strerr_diefu1sys(111, "stralloc_catb")
+#define dieout() strerr_diefu1sys(111, "write to stdout")
+
+#define HEADER1 "Status: 200\nContent-Type: text/html\n\n\
+<html>\n\
+<head>\n\
+  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\
+  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n\
+  <meta http-equiv=\"Content-Language\" content=\"en\" />\n\
+  <title>index of directory at "
+
+#define HEADER2 "</title>\n\
+  <meta name=\"Description\" content=\"directory entries list, generated by index.cgi\" />\n\
+  <meta name=\"Keywords\" content=\"tipideed index index.cgi CGI directory web server skarnet.org skarnet software\" />\n\
+  </head>\n\
+<body>\n\
+<h1> Directory entries for "
+
+#define HEADER3 " </h1>\n<ul>\n"
+
+#define FOOTER "</ul>\n</body>\n</html>\n"
+
+static void print_one_entry (int dfd, char const *path, char const *name)
+{
+  struct stat st ;
+  if (stat_at(dfd, name, &st) == -1) strerr_diefu4sys(111, "stat ", path, "/", name) ;
+  if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) return ;
+  if (buffer_puts(buffer_1, " <li> <a href=\"") == -1
+   || buffer_puts(buffer_1, name) == -1
+   || (S_ISDIR(st.st_mode) && buffer_put(buffer_1, "/", 1) == -1)
+   || buffer_puts(buffer_1, "\"> ") == -1
+   || buffer_puts(buffer_1, name) == -1
+   || (S_ISDIR(st.st_mode) && buffer_put(buffer_1, "/", 1) == -1)
+   || buffer_puts(buffer_1, " </a>") == -1) dieout() ;
+  if (!S_ISDIR(st.st_mode))
+  {
+    int iskB = st.st_size >= 1024 ;
+    char lmdate[128] ;
+    char fmt[UINT64_FMT] ;
+    tain t ;
+    size_t l ;
+    if (!tain_from_timespec(&t, &st.st_mtim))
+      strerr_diefu4sys(111, "convert st_mtim for ", path, "/", name) ;
+    l = tipidee_response_header_date_fmt(lmdate, 128, &t) ;
+    if (!l) strerr_diefu4sys(111, "convert st_mtim for ", path, "/", name) ;
+    lmdate[l++] = 0 ;
+    fmt[uint64_fmt(fmt, iskB ? st.st_size / 1024 : st.st_size)] = 0 ;
+    if (buffer_put(buffer_1, " (", 2) == -1
+     || buffer_puts(buffer_1, fmt) == -1
+     || buffer_put(buffer_1, " ", 1) == -1
+     || buffer_puts(buffer_1, iskB ? "kB" : "bytes") == -1
+     || buffer_puts(buffer_1, ", last modified ") == -1
+     || buffer_put(buffer_1, lmdate, l) == -1
+     || buffer_put(buffer_1, ")", 1) == -1) dieout() ;
+  }
+  if (buffer_puts(buffer_1, " </li>\n") == -1) dieout() ;
+}
+
+static void html_escape (stralloc *sa, char const *s)
+{
+  size_t len = strlen(s) ;
+  char cc[2] = "\0" ;
+  for (size_t i = 0 ; i < len ; i++)
+  {
+    cc[0] = s[i] ;
+    if (!stralloc_cats(sa, tipidee_util_htmlescape(cc))) dienomem() ;
+  }
+  if (!stralloc_0(sa)) dienomem() ;
+}
+
+int main (int argc, char const *const *argv, char const *const *envp)
+{
+  stralloc sa = STRALLOC_ZERO ;
+  stralloc tmp = STRALLOC_ZERO ;
+  size_t dirlen ;
+  char const *x ;
+  DIR *dir ;
+  int dfd ;
+  PROG = "ls.cgi" ;
+
+  x = getenv("SERVER_PROTOCOL") ;
+  if (!x) strerr_dienotset(100, "SERVER_PROTOCOL") ;
+  if (strncmp(x, "HTTP/1.", 7))
+    strerr_diefu1x(100, "SERVER_PROTOCOL isn't HTTP") ;
+  x = getenv("REQUEST_METHOD") ;
+  if (!x) strerr_dienotset(100, "REQUEST_METHOD") ;
+  if (strcmp(x, "GET") && strcmp(x, "HEAD"))
+    strerr_diefu1x(100, "ls.cgi can only be used with the GET or HEAD methods") ;
+
+  x = getenv("SERVER_NAME") ;
+  if (!x) strerr_dienotset(100, "SERVER_NAME") ;
+  if (!stralloc_cats(&sa, x)) dienomem() ;
+  if (!stralloc_catb(&sa, ":", 1)) dienomem() ;
+  x = getenv("SERVER_PORT") ;
+  if (!x) strerr_dienotset(100, "SERVER_PORT") ;
+  if (!stralloc_cats(&sa, x)) dienomem() ;
+  x = getenv("SCRIPT_NAME") ;
+  if (!x) strerr_dienotset(100, "SCRIPT_NAME") ;
+  if (*x != '/') strerr_diefu2x(100, "SCRIPT_NAME", " must be absolute") ;
+  if (!stralloc_cats(&sa, x)) dienomem() ;
+  while (sa.s[sa.len - 1] != '/') sa.len-- ;
+  sa.s[--sa.len] = 0 ;
+  html_escape(&tmp, sa.s) ;
+  dirlen = tmp.len - 1 ;
+
+  dir = opendir(sa.s) ;
+  if (!dir) strerr_diefu2sys(111, "opendir ", sa.s) ;
+  dfd = dir_fd(dir) ;
+
+  if (buffer_puts(buffer_1, HEADER1) == -1
+   || buffer_put(buffer_1, tmp.s, dirlen) == -1
+   || buffer_puts(buffer_1, HEADER2) == -1
+   || buffer_put(buffer_1, tmp.s, dirlen) == -1
+   || buffer_puts(buffer_1, HEADER3) == -1) dieout() ;
+
+  for (;;)
+  {
+    direntry *d ;
+    errno = 0 ;
+    d = readdir(dir) ;
+    if (!d) break ;
+    if (d->d_name[0] == '.') continue ;
+    html_escape(&tmp, d->d_name) ;
+    print_one_entry(dfd, tmp.s, tmp.s + dirlen + 1) ;
+    tmp.len = dirlen + 1 ;
+  }
+  if (errno) strerr_diefu2sys(111, "readdir ", sa.s) ;
+  dir_close(dir) ;
+
+  if (buffer_putsflush(buffer_1, FOOTER) == -1) dieout() ;
+  return 0 ;
+}