Compare commits

...

21 Commits

Author SHA1 Message Date
Solene Rapenne 12010338c2 we don't support -m from vger 10 months ago
Solene Rapenne 1e8d353859 todo 10 months ago
Solene Rapenne 872aeffe77 use Network.URI instead of a regex 10 months ago
Solene Rapenne 9144140aff bump licence 10 months ago
Solene Rapenne 9816876c62 remove shell.nix 10 months ago
Solene Rapenne 014b9efb64 mention hix 10 months ago
Solene Rapenne caaca49a54 integration tests should use the new vger path 10 months ago
Solene Rapenne 48ab9299d8 use hix project framework 10 months ago
Solene Rapenne 92f356e09f improve gemini protocol cases 10 months ago
Solene Rapenne 1809a0d13a move file 10 months ago
Solene Rapenne 95a2ef910e apply ormolu for formatting 10 months ago
Solene Rapenne 32bee65a20 support code 31 for directories 10 months ago
Solene Rapenne 74803a76ca support index 10 months ago
Solene Rapenne 9a145bb042 add getopt to support language 10 months ago
Solene Rapenne aa900ac405 unit tests for mime 10 months ago
Solene Rapenne d8c9d26e30 use uppercase for unit file 10 months ago
Solene Rapenne 56740f7f14 start cleaning up the README 10 months ago
Solene Rapenne aa3aff9854 remove C bits from Procfile 10 months ago
Solene Rapenne bb1de0e0cc more unit tests 10 months ago
Solene Rapenne 0b1d5d399d haskell mode 10 months ago
Solene Rapenne 7212128831 init version haskell 10 months ago

5
.gitignore vendored

@ -1,2 +1,3 @@
*.o
vger
*.hi
Vger
unit

@ -1,4 +1,4 @@
Copyright (c) 2020-2021 Solène Rapenne <solene@openbsd.org>
Copyright (c) 2020-2022 Solène Rapenne <solene@openbsd.org>
BSD 2-clause License

@ -1,33 +0,0 @@
include config.mk
PREFIX?=/usr/local/
CFLAGS += -pedantic -Wall -Wextra -Wmissing-prototypes \
-Wstrict-prototypes -Wwrite-strings ${EXTRAFLAGS}
.SUFFIXES: .c .o
.c.o:
${CC} ${CFLAGS} -c $<
all: vger
clean:
find . \( -name vger -o \
-name unit_test -o \
-name "*.o" -o \
-name "*.core" \) \
-delete
vger: main.c vger.c mimes.o utils.o opts.h
${CC} ${CFLAGS} -o $@ main.c mimes.o utils.o
install: vger
install -o root -g wheel vger ${PREFIX}/bin/
install -o root -g wheel vger.8 ${PREFIX}/man/man8/
unit_test: tests.c vger.o
${CC} ${CFLAGS} -o $@ vger.o tests.c mimes.o utils.o
test: vger unit_test
./unit_test
cd tests && sh test.sh

@ -31,12 +31,14 @@ For all supported OS, it's possible to run **Vger** in a chroot and drop privile
```
git clone https://tildegit.org/solene/vger.git
cd vger
./configure (only really useful for Linux)
make
doas make install
nix build
```
On GNU/Linux, make sure you installed `libbsd`, it has been reported that using clang was required too.
# Development
```
nix run .#ghcid -- . Main main tests
```
For NixOS/Nix users, there is a `shell.nix` listing the dependencies.

@ -0,0 +1,41 @@
{-# Language ApplicativeDo #-}
{-# Language RecordWildCards #-}
import Gemini
import Options.Applicative
data Sample = Sample
{ baseDir :: String
, language :: String
, virtualhost :: Bool
}
sample :: Parser Sample
sample = do
baseDir <- strOption
( long "baseDir"
<> short 'd'
<> help "base directory to serve files from"
<> value "/var/gemini/")
language <- strOption
( long "language"
<> short 'l'
<> help "language to use in the response for gemini files"
<> value "")
virtualhost <- switch
( long "virtualhost"
<> short 'v'
<> help "virtualhost support")
pure Sample{..}
opts :: ParserInfo Sample
opts = info (sample <**> helper)
(fullDesc)
main :: IO ()
main = do
options <- execParser opts
request <- get_request
answer <- create_answer request (language options) (baseDir options) (virtualhost options)
putStr (make_reply answer)
putStr (content answer)

@ -1 +0,0 @@
EXTRAFLAGS=

14
configure vendored

@ -1,14 +0,0 @@
#!/bin/sh
OS="$(uname -s)"
case "$OS" in
Linux)
EXTRAFLAGS=-lbsd
;;
*)
EXTRAFLAGS=""
;;
esac
echo "EXTRAFLAGS=${EXTRAFLAGS}" > config.mk

@ -0,0 +1,248 @@
{
"nodes": {
"easy-hls": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1646146030,
"narHash": "sha256-wkKDVVL2/rUGokxs4kqCE+ZzxljBqYDZvBuQGVGXYJM=",
"owner": "jkachmar",
"repo": "easy-hls-nix",
"rev": "ecb85ab6ba0aab0531fff32786dfc51feea19370",
"type": "github"
},
"original": {
"owner": "jkachmar",
"repo": "easy-hls-nix",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"hix": {
"inputs": {
"easy-hls": "easy-hls",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2",
"nixpkgs_ghc8107": "nixpkgs_ghc8107",
"nixpkgs_ghc865": "nixpkgs_ghc865",
"nixpkgs_ghc884": "nixpkgs_ghc884",
"nixpkgs_ghc901": "nixpkgs_ghc901",
"nixpkgs_ghc902": "nixpkgs_ghc902",
"nixpkgs_ghc922": "nixpkgs_ghc922",
"nmd": "nmd",
"obelisk": "obelisk",
"thax": "thax"
},
"locked": {
"lastModified": 1658021306,
"narHash": "sha256-chR2NPBuJgI/aaN5IdH83DabILJQ8AEfjRsDlhkEQiM=",
"owner": "tek",
"repo": "hix",
"rev": "bedd71655b5fe8af681d28f197dff483edfc2f19",
"type": "github"
},
"original": {
"owner": "tek",
"repo": "hix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1615055905,
"narHash": "sha256-Ig7CXgE5C3mwtTVBl8nSTessa1oMAm6Pm/p+T6iAJD8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f3fc074642a25ef5a1423412f09946149237e338",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1651927509,
"narHash": "sha256-fGVGUdEsriuAL1vkUh29FlOQmEkPRnSfRGImWYaVjos=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fdb6f2e08e7989b03a2a1aa8538d99e3eeea881",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fdb6f2e08e7989b03a2a1aa8538d99e3eeea881",
"type": "github"
}
},
"nixpkgs_ghc8107": {
"locked": {
"lastModified": 1642069818,
"narHash": "sha256-666w6j8wl/bojfgpp0k58/UJ5rbrdYFbI2RFT2BXbSQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "46821ea01c8f54d2a20f5a503809abfc605269d7",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "46821ea01c8f54d2a20f5a503809abfc605269d7",
"type": "github"
}
},
"nixpkgs_ghc865": {
"locked": {
"lastModified": 1602473909,
"narHash": "sha256-tZfZhmjYNX4UrUySEjbosJO7Bj3eiCGnjfX5DkVClQw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cfed29bfcb28259376713005d176a6f82951014a",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cfed29bfcb28259376713005d176a6f82951014a",
"type": "github"
}
},
"nixpkgs_ghc884": {
"locked": {
"lastModified": 1616670887,
"narHash": "sha256-wn+l9qJfR5sj5Gq4DheJHAcBDfOs9K2p9seW2f35xzs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c0e881852006b132236cbf0301bd1939bb50867e",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c0e881852006b132236cbf0301bd1939bb50867e",
"type": "github"
}
},
"nixpkgs_ghc901": {
"locked": {
"lastModified": 1639713555,
"narHash": "sha256-w1TacWjnqhC19n+rheyOif3JxwvWMbyxfgqYCY0FLdQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "45a3f9d7725c7e21b252c223676cc56fb2ed5d6d",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "45a3f9d7725c7e21b252c223676cc56fb2ed5d6d",
"type": "github"
}
},
"nixpkgs_ghc902": {
"locked": {
"lastModified": 1642069818,
"narHash": "sha256-666w6j8wl/bojfgpp0k58/UJ5rbrdYFbI2RFT2BXbSQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "46821ea01c8f54d2a20f5a503809abfc605269d7",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "46821ea01c8f54d2a20f5a503809abfc605269d7",
"type": "github"
}
},
"nixpkgs_ghc922": {
"locked": {
"lastModified": 1651927509,
"narHash": "sha256-fGVGUdEsriuAL1vkUh29FlOQmEkPRnSfRGImWYaVjos=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2fdb6f2e08e7989b03a2a1aa8538d99e3eeea881",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2fdb6f2e08e7989b03a2a1aa8538d99e3eeea881",
"type": "github"
}
},
"nmd": {
"flake": false,
"locked": {
"lastModified": 1648078774,
"narHash": "sha256-rWaMxlODw2uji55ANn+t0/20elreUGvvY9FaRNbBJMo=",
"ref": "master",
"rev": "de522bdd533350b3afb41e1ce9b3afb72922fba2",
"revCount": 31,
"type": "git",
"url": "https://gitlab.com/rycee/nmd"
},
"original": {
"type": "git",
"url": "https://gitlab.com/rycee/nmd"
}
},
"obelisk": {
"flake": false,
"locked": {
"lastModified": 1650800768,
"narHash": "sha256-bBr61MzVFyIMpsri8inOPxCHSL1ogFbpb3uGS7pZ+X0=",
"owner": "tek",
"repo": "obelisk",
"rev": "7991b4d86be0ae04e22bcbe91607d99eb113c2c2",
"type": "github"
},
"original": {
"owner": "tek",
"ref": "ghc9",
"repo": "obelisk",
"type": "github"
}
},
"root": {
"inputs": {
"hix": "hix"
}
},
"thax": {
"locked": {
"lastModified": 1650770034,
"narHash": "sha256-OI4dxtNKgXogHBwYnNAfDABhHEcmWiBkkboZ+cCfyzk=",
"owner": "tek",
"repo": "thax",
"rev": "dc0025eebaba4ad97fcf48b68c14c8901d90daa5",
"type": "github"
},
"original": {
"owner": "tek",
"repo": "thax",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

@ -0,0 +1,60 @@
{
description = "vger";
inputs.hix.url = github:tek/hix;
outputs = { hix, ... }:
hix.lib.flake {
ghci.extensions = hix.inputs.nixpkgs.lib.mkForce [];
ghci.args = hix.inputs.nixpkgs.lib.mkForce [];
base = ./.;
packages = { vger = ./.; };
hpack.packages.vger = {
name = "vger";
executables.vger = {
ghc-options = [
"-Wunused-packages"
];
main = "Vger.hs";
source-dirs = "app";
dependencies = [
"vger"
"base"
"optparse-applicative"
"network-uri"
];
};
library = {
ghc-options = [
"-Wunused-packages"
];
source-dirs = "lib";
dependencies = [
"base"
"regex-pcre"
"regex-compat"
"mime-types"
"utf8-string"
"directory"
"text"
"network-uri"
];
};
tests.test = {
ghc-options = [
"-Wunused-packages"
];
main = "Main.hs";
source-dirs = "tests";
dependencies = [
"vger"
"base"
"HUnit"
"network-uri"
];
};
};
};
}

@ -0,0 +1,138 @@
module Gemini where
import Control.Exception
import Data.ByteString.UTF8
import Data.Text
import Network.Mime
import Network.URI
import System.Directory (doesDirectoryExist, doesFileExist)
import Text.Regex
import Text.Regex.PCRE
getFile :: FilePath -> IO (Maybe String)
getFile s = do
exists <- doesFileExist s
case exists of
True -> do
result <- try (readFile s) :: IO (Either SomeException String)
case result of
Left ex -> return Nothing
Right ex -> return (Just ex)
False -> return Nothing
-- remove any .. in the uri that could escape the location
sanitize_uri :: String -> String
sanitize_uri path =
subRegex (mkRegex "\\.\\.\\/") path ""
remove_crnlf :: String -> String
remove_crnlf uri =
subRegex (mkRegex "\r\n$") uri ""
-- return a file as a string
read_file :: String -> IO String
read_file path = do
text <- readFile path
pure text
-- read from stdin
get_request :: IO String
get_request = do
stdin <- getContents
pure stdin
data Answer = MkAnswer
{ code :: Int,
lang :: String,
content :: String,
mime :: String
}
get_mime :: String -> String
get_mime filename
| (fileNameExtensions (pack filename)) == [(pack "gmi" :: Extension)] = "text/gemini"
| (fileNameExtensions (pack filename)) == [(pack "gemini" :: Extension)] = "text/gemini"
| (fileNameExtensions (pack filename)) == [(pack "md" :: Extension)] = "text/markdown"
| otherwise = (toString (defaultMimeLookup (pack filename)))
geminiInvalidURI :: Answer
geminiInvalidURI =
MkAnswer
{ code = 50,
lang = "",
mime = "",
content = ""
}
create_answer :: String -> String -> String -> Bool -> IO Answer
create_answer request language baseDir vhost = do
-- parse the URI
case parseURI (remove_crnlf (sanitize_uri request)) of
Nothing -> return geminiInvalidURI
Just uri -> do
-- look for authority info
case uriAuthority uri of
Nothing -> pure geminiInvalidURI
-- request is valid from here
Just auth -> do
let domain = uriRegName auth
let file = uriPath uri
let baseDir' =
if vhost
then baseDir ++ "/" ++ domain
else baseDir
content <- getFile $ baseDir' ++ file
case content of
-- file has been found
Just x ->
return
( MkAnswer
{ code = 20,
lang = language,
mime = get_mime file,
content = x
}
)
Nothing -> do
-- no file
-- try /index.gmi
content' <- getFile $ baseDir' ++ file ++ "index.gmi"
case content' of
Just y ->
return
( MkAnswer
{ code = 20,
lang = language,
mime = get_mime $ file ++ "index.gmi",
content = y
}
)
Nothing -> do
isdir <- doesDirectoryExist $ baseDir' ++ file
if isdir
then
return
( MkAnswer
{ code = 31,
lang = "",
mime = file ++ "/",
content = ""
}
)
else
return
( MkAnswer
{ code = 52,
lang = language,
mime = "",
content = "can't find " ++ baseDir' ++ file
}
)
make_reply :: Answer -> String
make_reply (MkAnswer code lang content mime)
| code == 20 && mime == "text/gemini" && lang /= "" = "20 " ++ mime ++ "; lang=" ++ lang ++ " \r\n"
| code == 20 && mime == "text/gemini" && lang == "" = "20 " ++ mime ++ "; \r\n"
| code == 20 && mime /= "text/gemini" = "20 " ++ mime ++ "\r\n"
| code == 31 = "31 " ++ mime ++ "\r\n"
| otherwise = "55\r\n"

@ -0,0 +1,3 @@
-- before reading a file, check if it's a symbolic link and if so, read the target
-- cffi foreign import

@ -1,88 +0,0 @@
#include "vger.c"
int
main(int argc, char **argv)
{
char request[GEMINI_REQUEST_MAX] = {'\0'};
char user[_SC_LOGIN_NAME_MAX] = {'\0'};
char hostname[GEMINI_REQUEST_MAX] = {'\0'};
char query[PATH_MAX] = {'\0'};
char path[PATH_MAX] = {'\0'};
char chroot_dir[PATH_MAX] = DEFAULT_CHROOT;
char file[FILENAME_MAX] = DEFAULT_INDEX;
char dir[PATH_MAX] = {'\0'};
int option = 0;
int virtualhost = 0;
/*
* request : contain the whole request from client : gemini://...\r\n
* user : username, used in drop_privileges()
* hostname : extracted from hostname. used with virtualhosts and cgi SERVER_NAME
* query : file requested in cgi : gemini://...?query
* file : file basename to display. Emtpy is a directory has been requested
* dir : directory requested. vger will chdir() in to find file
* pos : used to parse request and split into interesting parts
*/
while ((option = getopt(argc, argv, ":d:l:m:u:c:vi")) != -1) {
switch (option) {
case 'd':
estrlcpy(chroot_dir, optarg, sizeof(chroot_dir));
break;
case 'l':
estrlcpy(lang, "lang=", sizeof(lang));
estrlcat(lang, optarg, sizeof(lang));
break;
case 'm':
estrlcpy(default_mime, optarg, sizeof(default_mime));
break;
case 'u':
estrlcpy(user, optarg, sizeof(user));
break;
case 'c':
estrlcpy(cgi_dir, optarg, sizeof(cgi_dir));
break;
case 'v':
virtualhost = 1;
break;
case 'i':
doautoidx = 1;
break;
}
}
/*
* do chroot if an user is supplied
*/
drop_privileges(user, chroot_dir, cgi_dir);
check_request(request);
get_hostname(request, hostname, sizeof(hostname));
get_path(request, path, sizeof(path), virtualhost, hostname);
get_query(path, query, sizeof(query));
/* percent decode */
uridecode(query);
uridecode(path);
/* is it cgi ? */
if (*cgi_dir)
if (do_cgi(chroot_dir, cgi_dir, path, hostname, query) == 0)
stop(EXIT_SUCCESS, NULL);
/* *** from here, cgi didn't run ***
* check if path available
*/
check_path(path, sizeof(path), hostname, virtualhost);
/* split dir and filename */
get_dir_file(path, dir, sizeof(dir), file, sizeof(file));
/* go to dir */
echdir(dir);
/* regular file to stdout */
display_file(file);
stop(EXIT_SUCCESS, NULL);
}

@ -1,139 +0,0 @@
#include <string.h>
#include <unistd.h>
#include <string.h>
#include "mimes.h"
#include "opts.h"
/* extension to mimetype table */
static const struct {
const char *extension;
const char *type;
} database[] = {
{"gmi", "text/gemini"},
{"gemini", "text/gemini"},
{"7z", "application/x-7z-compressed"},
{"atom", "application/atom+xml"},
{"avi", "video/x-msvideo"},
{"bin", "application/octet-stream"},
{"bmp", "image/x-ms-bmp"},
{"cco", "application/x-cocoa"},
{"crt", "application/x-x509-ca-cert"},
{"css", "text/css"},
{"deb", "application/octet-stream"},
{"dll", "application/octet-stream"},
{"dmg", "application/octet-stream"},
{"doc", "application/msword"},
{"eot", "application/vnd.ms-fontobject"},
{"exe", "application/octet-stream"},
{"flv", "video/x-flv"},
{"fs", "application/octet-stream"},
{"gif", "image/gif"},
{"hqx", "application/mac-binhex40"},
{"htc", "text/x-component"},
{"html", "text/html"},
{"ico", "image/x-icon"},
{"img", "application/octet-stream"},
{"iso", "application/octet-stream"},
{"jad", "text/vnd.sun.j2me.app-descriptor"},
{"jar", "application/java-archive"},
{"jardiff", "application/x-java-archive-diff"},
{"jng", "image/x-jng"},
{"jnlp", "application/x-java-jnlp-file"},
{"jpeg", "image/jpeg"},
{"jpg", "image/jpeg"},
{"js", "application/javascript"},
{"json", "application/json"},
{"kml", "application/vnd.google-earth.kml+xml"},
{"kmz", "application/vnd.google-earth.kmz"},
{"m3u8", "application/vnd.apple.mpegurl"},
{"m4a", "audio/x-m4a"},
{"m4v", "video/x-m4v"},
{"md", "text/markdown"},
{"mid", "audio/midi"},
{"midi", "audio/midi"},
{"mkv", "video/x-matroska"},
{"mml", "text/mathml"},
{"mng", "video/x-mng"},
{"mov", "video/quicktime"},
{"mp3", "audio/mpeg"},
{"mp4", "video/mp4"},
{"mpeg", "video/mpeg"},
{"mpg", "video/mpeg"},
{"msi", "application/octet-stream"},
{"msm", "application/octet-stream"},
{"msp", "application/octet-stream"},
{"odb", "application/vnd.oasis.opendocument.database"},
{"odc", "application/vnd.oasis.opendocument.chart"},
{"odf", "application/vnd.oasis.opendocument.formula"},
{"odg", "application/vnd.oasis.opendocument.graphics"},
{"odi", "application/vnd.oasis.opendocument.image"},
{"odm", "application/vnd.oasis.opendocument.text-master"},
{"odp", "application/vnd.oasis.opendocument.presentation"},
{"ods", "application/vnd.oasis.opendocument.spreadsheet"},
{"odt", "application/vnd.oasis.opendocument.text"},
{"ogg", "audio/ogg"},
{"oth", "application/vnd.oasis.opendocument.text-web"},
{"otp", "application/vnd.oasis.opendocument.presentation-template"},
{"pac", "application/x-ns-proxy-autoconfig"},
{"pdf", "application/pdf"},
{"pem", "application/x-x509-ca-cert"},
{"pl", "application/x-perl"},
{"pm", "application/x-perl"},
{"png", "image/png"},
{"ppt", "application/vnd.ms-powerpoint"},
{"ps", "application/postscript"},
{"ra", "audio/x-realaudio"},
{"rar", "application/x-rar-compressed"},
{"rpm", "application/x-redhat-package-manager"},
{"rss", "application/rss+xml"},
{"rtf", "application/rtf"},
{"run", "application/x-makeself"},
{"sea", "application/x-sea"},
{"sit", "application/x-stuffit"},
{"svg", "image/svg+xml"},
{"svgz", "image/svg+xml"},
{"swf", "application/x-shockwave-flash"},
{"tcl", "application/x-tcl"},
{"tif", "image/tiff"},
{"tiff", "image/tiff"},
{"tk", "application/x-tcl"},
{"ts", "video/mp2t"},
{"txt", "text/plain"},
{"war", "application/java-archive"},
{"wbmp", "image/vnd.wap.wbmp"},
{"webm", "video/webm"},
{"webp", "image/webp"},
{"wml", "text/vnd.wap.wml"},
{"wmlc", "application/vnd.wap.wmlc"},
{"wmv", "video/x-ms-wmv"},
{"woff", "application/font-woff"},
{"xhtml", "application/xhtml+xml"},
{"xls", "application/vnd.ms-excel"},
{"xml", "text/xml"},
{"xpi", "application/x-xpinstall"},
{"zip", "application/zip"}
};
#ifndef nitems
#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
#endif
const char *
get_file_mime(const char *path, const char *default_mime)
{
size_t i;
char *extension;
/* search for extension after last '.' in path */
if ((extension = strrchr(path, '.')) != NULL) {
/* look for the MIME in the database */
for (i = 0; i < nitems(database); i++) {
if (strcmp(database[i].extension, extension + 1) == 0)
return (database[i].type);
}
}
/* no MIME found, set a default one */
return default_mime;
}

@ -1 +0,0 @@
const char *get_file_mime(const char *, const char *);

@ -1,18 +0,0 @@
#include <limits.h> /* PATH_MAX */
/* Defaults values */
#define DEFAULT_MIME "application/octet-stream"
#define DEFAULT_LANG ""
#define DEFAULT_CHROOT "/var/gemini"
#define DEFAULT_INDEX "index.gmi"
#define DEFAULT_AUTOIDX 0
/*
* Options used later
*/
/* longest hardcoded mimetype is 56 long so 64 should be enough */
static char default_mime[64] = DEFAULT_MIME;
static char lang[16] = DEFAULT_LANG;
static unsigned int doautoidx = DEFAULT_AUTOIDX;
static char cgi_dir[PATH_MAX] = {'\0'};
static int chrooted = 0;

@ -1,4 +0,0 @@
with (import <nixpkgs> {});
mkShell {
buildInputs = [ gcc libbsd gnumake ];
}

@ -1,50 +0,0 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "vger.h"
// to test
void test_status(void);
void test_status_error(void);
void test_uridecode(char*, const int);
void
test_uridecode(char *str, const int result)
{
char reference[GEMINI_REQUEST_MAX] = {'\0'};
strlcpy(reference, str, sizeof(reference));
uridecode(str);
if (strncmp(reference, str, strlen(str)) != result)
{
printf("uridecode error\n");
printf("Strings should be %s\n", (result == 0) ? "identical" : "different");
printf("passed %s\n", reference);
printf("got %s\n", str);
exit(1);
}
}
void
test_status(void)
{
status(20, "text/gemini");
}
void
test_status_error(void)
{
status(51, "file not found");
status(50, "Forbidden path");
status(50, "Internal server error");
}
int
main(void)
{
test_status_error();
test_status();
//test_uridecode("host.name", 0);
//test_uridecode("host.name/percent%25-encode%3.gmi", 1);
return(0);
}

@ -0,0 +1,41 @@
module Main where
import Test.HUnit
import Gemini
regex_10 = TestCase (assertEqual
"ensure sanitization works"
"gemini://perso.pw/main.gmi/passwd"
(sanitize_uri "gemini://perso.pw/../../../main.gmi/../../passwd"))
mime_1 = TestCase (assertEqual
"gmi files should be text/gemini"
"text/gemini"
(get_mime "main.gmi"))
mime_2 = TestCase (assertEqual
"markdown file"
"text/plain"
(get_mime "file.txt"))
mime_3 = TestCase (assertEqual
"jpeg file"
"image/jpeg"
(get_mime "picture.jpg"))
mime_4 = TestCase (assertEqual
"png file"
"image/png"
(get_mime "picture.png"))
tests = TestList
[ regex_10
, mime_1
, mime_2
, mime_3
, mime_4
]
main :: IO Counts
main = do
runTestTT tests

@ -12,113 +12,109 @@ MD5()
}
# serving a file
OUT=$(printf "gemini://host.name/main.gmi\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/main.gmi\r\n" | ../result/bin/vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "c7e352d6aae4ee7e7604548f7874fb9d" ] ; then echo "error" ; exit 1 ; fi
# default index.gmi file
OUT=$(printf "gemini://host.name\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name\r\n" | ../result/bin/vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
# default index.gmi file when using a trailing slash
OUT=$(printf "gemini://host.name/\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/\r\n" | ../result/bin/vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
# default index.gmi file when client specify port
OUT=$(printf "gemini://host.name:1965\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name:1965\r\n" | ../result/bin/vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
# redirect to uri with trailing / if directory
OUT=$(printf "gemini://host.name/subdir\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/subdir\r\n" | ../result/bin/vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "84e5e7bb3eee0dfcc8db14865dc83e77" ] ; then echo "error" ; exit 1 ; fi
# redirect to uri with trailing / if directory and vhost enabled
OUT=$(printf "gemini://perso.pw/cgi-bin\r\n" | ../vger -vd var/gemini | tee /dev/stderr | MD5)
OUT=$(printf "gemini://perso.pw/cgi-bin\r\n" | ../result/bin/vger -v -d var/gemini | tee /dev/stderr | MD5)
if ! [ $OUT = "e0eb3a8e31bdb30c89d92d1d2b0a1fa1" ] ; then echo "error" ; exit 1 ; fi
# file from local directory with lang=fr and markdown MIME type
OUT=$(printf "gemini://perso.pw/file.md\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
OUT=$(printf "gemini://perso.pw/file.md\r\n" | ../result/bin/vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
if ! [ $OUT = "09c82ffe243ce3b3cfb04c2bc4a91acb" ] ; then echo "error" ; exit 1 ; fi
# file from local directory with lang=fr and unknown MIME type (default to application/octet-stream)
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../result/bin/vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
if ! [ $OUT = "2c73bfb33dd2d12be322ebb85e03c015" ] ; then echo "error" ; exit 1 ; fi
# file from local directory and unknown MIME type, default forced to text/plain
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -m text/plain | tee /dev/stderr | MD5)
if ! [ $OUT = "8169f43fbb2032f4054b153c38fe61d6" ] ; then echo "error" ; exit 1 ; fi
# redirect file
OUT=$(printf "gemini://perso.pw/old_location\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://perso.pw/old_location\r\n" | ../result/bin/vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "cb4597b6fcc82cbc366ac9002fb60dac" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts
OUT=$(printf "gemini://perso.pw/index.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://perso.pw/index.gmi\r\n" | ../result/bin/vger -v -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts without specifying a file
OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://perso.pw\r\n" | ../result/bin/vger -v -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts without specifying a file using lang = fr
OUT=$(printf "gemini://perso.pw/\r\n" | ../vger -v -d var/gemini/ -l fr | tee /dev/stderr | MD5)
OUT=$(printf "gemini://perso.pw/\r\n" | ../result/bin/vger -v -d var/gemini/ -l fr | tee /dev/stderr | MD5)
if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts and IRI
OUT=$(printf "gemini://virtualhoßt/é è.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://virtualhoßt/é è.gmi\r\n" | ../result/bin/vger -v -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "282cee071d3bd20dbb6e6af38f217a29" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts and IRI both with emojis
OUT=$(printf "gemini://⛴//❤️.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "gemini://⛴//❤️.gmi\r\n" | ../result/bin/vger -v -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "e354a1a29ea8273faaf0cdc29c1d8583" ] ; then echo "error" ; exit 1 ; fi
# auto index in directory without index.gmi must redirect
OUT=$(printf "gemini://host.name/autoidx\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/autoidx\r\n" | ../result/bin/vger -d var/gemini/ -i | tee /dev/stderr | MD5)
if ! [ $OUT = "874f5e1af67eff6b93bedf8ac8033066" ] ; then echo "error" ; exit 1 ; fi
# auto index in directory
OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../result/bin/vger -d var/gemini/ -i | tee /dev/stderr | MD5)
if ! [ $OUT = "765bbbe2add810be8eb191bbde59e258" ] ; then echo "error" ; exit 1 ; fi
# cgi simple script
OUT=$(printf "gemini://host.name/cgi-bin/test.cgi\r\n" | ../vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/cgi-bin/test.cgi\r\n" | ../result/bin/vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "666e48200f90018b5e96c2cf974882dc" ] ; then echo "error" ; exit 1 ; fi
# cgi with use of variables
OUT=$(printf "gemini://host.name/cgi-bin/who.cgi?user=jean-mi\r\n" | ../vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/cgi-bin/who.cgi?user=jean-mi\r\n" | ../result/bin/vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "fa065a67d1f7c973501d4a9e3ca2ea57" ] ; then echo "error" ; exit 1 ; fi
# cgi with error
OUT=$(printf "gemini://host.name/cgi-bin/nope\r\n" | ../vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/cgi-bin/nope\r\n" | ../result/bin/vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "31b98e160402a073298c12f763d5db64" ] ; then echo "error" ; exit 1 ; fi
# cgi with PATH_INFO
OUT=$(printf "gemini://host.name/cgi-bin/test.cgi/path/info\r\n" | ../vger -d var/gemini -c var/gemini/cgi-bin | tee /dev/stderr | MD5)
OUT=$(printf "gemini://host.name/cgi-bin/test.cgi/path/info\r\n" | ../result/bin/vger -d var/gemini -c var/gemini/cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "ec64da76dc578ffb479fbfb23e3a7a5b" ] ; then echo "error" ; exit 1 ; fi
# virtualhost + cgi
OUT=$(printf "gemini://perso.pw/cgi-bin/test.cgi\r\n" | ../vger -v -d var/gemini/ -c var/gemini/perso.pw/cgi-bin | tee /dev/stderr | MD5)
OUT=$(printf "gemini://perso.pw/cgi-bin/test.cgi\r\n" | ../result/bin/vger -v -d var/gemini/ -c var/gemini/perso.pw/cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "666e48200f90018b5e96c2cf974882dc" ] ; then echo "error" ; exit 1 ; fi
# percent-decoding
OUT=$(printf "%s\r\n" "gemini://host.name/percent%25-encode%3f.gmi" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "%s\r\n" "gemini://host.name/percent%25-encode%3f.gmi" | ../result/bin/vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "83d59cca9ed7040145ac6df1992f5daf" ] ; then echo "error" ; exit 1 ; fi
# percent-decoding failing
OUT=$(printf "%s\r\n" "gemini://host.name/percent%25-encode%3.gmi" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
OUT=$(printf "%s\r\n" "gemini://host.name/percent%25-encode%3.gmi" | ../result/bin/vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "c782da4173898f57033a0804b8e96fc3" ] ; then echo "error" ; exit 1 ; fi
# must fail only on OpenBSD !
# try to escape from unveil
if [ -f /bsd ]
then
OUT=$(printf "gemini://fail_on_openbsd/../../test.sh\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
OUT=$(printf "gemini://fail_on_openbsd/../../test.sh\r\n" | ../result/bin/vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
if [ $OUT = "$( ( printf '20 text/gemini; lang=fr\r\n' ; cat $0) | MD5)" ] ; then echo "error" ; exit 1 ; fi
fi
#type doas 2>/dev/null
#if [ $? -eq 0 ]; then
# # file from local directory chroot
# OUT=$(printf "gemini://perso.pw\r\n" | doas ../vger -v -d var/gemini/ -u solene -l fr | tee /dev/stderr | MD5)
# OUT=$(printf "gemini://perso.pw\r\n" | doas ../result/bin/vger -v -d var/gemini/ -u solene -l fr | tee /dev/stderr | MD5)
# if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 ; fi
#fi

@ -1,117 +0,0 @@
#include <err.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <unistd.h>
#include "utils.h"
#include "vger.h"
#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined( __NetBSD__) || defined(__DragonFly__)
#include <string.h>
#else
#include <bsd/string.h>
#endif
/* e*foo() functions are the equivalent of foo() but handle errors.
* In case an error happens:
* The error is printed to stdout
* return 1
*/
#ifdef __OpenBSD__
void
eunveil(const char *path, const char *permissions)
{
if (unveil(path, permissions) == -1) {
status(41, "Error when unveil(), see logs");
stop(EXIT_FAILURE, "unveil on %s failed", path);
}
}
void
epledge(const char *promises, const char *execpromises)
{
if (pledge(promises, execpromises) == -1) {
status(41, "Error when pledge(), see logs");
stop(EXIT_FAILURE, "pledge failed for: %s", promises);
}
}
#endif
size_t
estrlcpy(char *dst, const char *src, size_t dstsize)
{
size_t n = 0;
n = strlcpy(dst, src, dstsize);
if (n >= dstsize) {
status(41, "strlcpy failed, see logs");
stop(EXIT_FAILURE, "strlcpy() failed for %s = %s", dst, src);
}
return n;
}
size_t
estrlcat(char *dst, const char *src, size_t dstsize)
{
size_t size;
if ((size = strlcat(dst, src, dstsize)) >= dstsize) {
status(41, "strlcat() failed, see logs");
stop(EXIT_FAILURE, "strlcat on %s + %s", dst, src);
}
return size;
}
int
esetenv(const char *name, const char *value, int overwrite)
{
int ret = 0;
ret = setenv(name, value, overwrite);
if (ret != 0) {
status(41, "setenv() failed, see logs");
stop(EXIT_FAILURE, "setenv() %s:%s", name, value);
}
return ret;
}
void
echdir(const char *path)
{
if (chdir(path) == -1) {
switch (errno) {
case ENOTDIR: /* FALLTHROUGH */
case ENOENT:
status(51, "file not found");
break;
case EACCES:
status(50, "Forbidden path");
break;
default:
status(50, "Internal server error");
break;
}
stop(EXIT_FAILURE, "chdir(%s) failed", path);
}
}
/* read the file fd byte after byte in buffer and write it to stdout
* return number of bytes read
*/
size_t
print_file(FILE *fd)
{
ssize_t nread = 0;
ssize_t datasent = 0;
char *buffer[BUFSIZ];
while ((nread = fread(buffer, 1, sizeof(buffer), fd)) != 0)
datasent += fwrite(buffer, 1, nread, stdout);
return datasent;
}

@ -1,8 +0,0 @@
void echdir (const char *);
void epledge(const char *, const char *);
void eunveil(const char *, const char *);
int esetenv(const char *, const char *, int);
size_t estrlcat(char *, const char *, size_t);
size_t estrlcpy(char *, const char *, size_t);
size_t print_file(FILE *fd);
void set_errmsg(const char *, ...);

505
vger.c

@ -1,505 +0,0 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <ctype.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "mimes.h"
#include "opts.h"
#include "utils.h"
#include "vger.h"
void
stop(const int r, const char *fmt, ...)
{
va_list ap, ap2;
fflush(stdout); /* ensure all data is sent */
/* log the request and retcode */
syslog(LOG_DAEMON, "\"%s\" %i %zd", _request, _retcode, _datasiz);
if (r != EXIT_SUCCESS) {
/* log and print error */
va_copy(ap2, ap);
va_start(ap, fmt);
vsyslog(LOG_ERR, fmt, ap);
va_end(ap);
va_start(ap2, fmt);
vfprintf(stderr, fmt, ap2);
va_end(ap2);
}
exit(r);
}
void
status(const int code, const char *fmt, ...)
{
va_list ap;
_datasiz += fprintf(stdout, "%i ", code);
va_start(ap, fmt);
_datasiz += vfprintf(stdout, fmt, ap);
va_end(ap);
_datasiz += fprintf(stdout, "\r\n"); /* make sure status end correctly */
_retcode = code; /* store return code for logs */
}
int
uridecode(char *uri)
{
int n = 0;
char c = '\0';
long l = 0;
char *pos = NULL;
if ((pos = strchr(uri, '%')) == NULL)
return n;
while ((pos = strchr(pos, '%')) != NULL) {
if (strlen(pos) < 3)
return n;
char hex[3] = {'\0'};
for (size_t i = 0; i < 2; i++)
hex[i] = tolower(pos[i + 1]);
errno = 0;
l = strtol(hex, 0, 16);
if (errno == ERANGE && (l == LONG_MAX || l == LONG_MIN))
continue; /* conversion failed */
c = (char)l;
pos[0] = c;
/* rewind of two char to remove %hex */
memmove(pos + 1, pos + 3, strlen(pos + 3) + 1); /* +1 for \0 */
n++;
pos++; /* avoid infinite loop */
}
return n;
}
void
drop_privileges(const char *user, const char *chroot_dir, const char *cgi_dir)
{
struct passwd *pw;
/*
* use chroot() if an user is specified requires root user to be
* running the program to run chroot() and then drop privileges
*/
if (*user) {
/* is root? */
if (getuid() != 0) {
status(41, "privileges issue, see logs");
stop(EXIT_FAILURE, "%s",
"chroot requires program to be run as root");
}
/* search user uid from name */
if ((pw = getpwnam(user)) == NULL) {
status(41, "privileges issue, see logs");
stop(EXIT_FAILURE,
"the user %s can't be found on the system", user);
}
/* chroot worked? */
if (chroot(chroot_dir) != 0) {
status(41, "privileges issue, see logs");
stop(EXIT_FAILURE,
"the chroot_dir %s can't be used for chroot", chroot_dir);
}
chrooted = 1;
echdir("/");
/* drop privileges */
if (setgroups(1, &pw->pw_gid) ||
setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) {
status(41, "privileges issue, see logs");
stop(EXIT_FAILURE,
"dropping privileges to user %s (uid=%i) failed", \
user, pw->pw_uid);
}
}
#ifdef __OpenBSD__
/*
* prevent access to files other than the one in chroot_dir
*/
if (chrooted)
eunveil("/", "r");
else
eunveil(chroot_dir, "r");
/* permission to execute what's inside cgi_dir */
if (*cgi_dir)
eunveil(cgi_dir, "rx");
eunveil(NULL, NULL); /* no more call to unveil() */
/* promise permissions */
if (*cgi_dir)
epledge("stdio rpath exec", NULL);
else
epledge("stdio rpath", NULL);
#endif
if (!chrooted)
echdir(chroot_dir); /* move to the gemini data directory */
}
ssize_t
display_file(const char *fname)
{
FILE *fd = NULL;
const char *file_mime;
/*
* special case : fname empty. The user requested just a dir name
*/
if ((strlen(fname) == 0) && (doautoidx)) {
/* no index.gmi, so display autoindex if enabled */
_datasiz += autoindex(".");
return _datasiz;
}
/* open the file requested */
if ((fd = fopen(fname, "r")) != NULL) {
file_mime = get_file_mime(fname, default_mime);
if (strcmp(file_mime, "text/gemini") == 0)
status(20, "%s; %s", file_mime, lang);
else
status(20, "%s", file_mime);
_datasiz += print_file(fd);
fclose(fd); /* close file descriptor */
} else {
/* return an error code and no content.
* seems unlikely to happen unless the file vanished
* since we checked with stat() if it exists
*/
status(51, "%s", "file not found and may have vanished");
}
return _datasiz;
}
int
do_cgi(const char *chroot_dir, const char *cgi_dir, const char *path, const char *hostname, const char *query)
{
/* WARNING : this function is fragile since it
* compares path using the string to access them.
* It would be preferable to use stat() to check
* if two path refer to the same inode
*/
char cgirp[PATH_MAX] = {'\0'}; /* cgi dir path in chroot */
char cgifp[PATH_MAX] = {'\0'}; /* cgi file to execute */
char *path_info = NULL;
/* check if path starts with cgi_dir
* compare beginning of path with cgi_dir
* path + 2 : skip "./"
* cgi_dir + strlen(chrootdir) (skip chrootdir)
*/
estrlcpy(cgirp, cgi_dir + strlen(chroot_dir), sizeof(cgirp));
/* ensure there is no leading / if user didn't end chrootdir with */
while (*cgirp =