├── .github └── workflows │ ├── ci.yml │ └── doc.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Setup.hs ├── default.nix ├── docs ├── bucktooth-gophermap.txt ├── man │ ├── spacecookie.1 │ ├── spacecookie.gophermap.5 │ └── spacecookie.json.5 ├── rfc1436.txt └── web.nix ├── etc ├── spacecookie.json ├── spacecookie.service └── spacecookie.socket ├── server ├── Main.hs └── Network │ └── Spacecookie │ ├── Config.hs │ ├── FileType.hs │ ├── Path.hs │ └── Systemd.hs ├── spacecookie.cabal ├── spacecookie.nix ├── src └── Network │ ├── Gopher.hs │ └── Gopher │ ├── Log.hs │ ├── Types.hs │ └── Util │ ├── Gophermap.hs │ └── Socket.hs └── test ├── EntryPoint.hs ├── Test ├── FileTypeDetection.hs ├── Gophermap.hs ├── Integration.hs └── Sanitization.hs ├── data ├── bucktooth.gophermap └── pygopherd.gophermap └── integration ├── root ├── .gophermap ├── dir │ ├── .hidden │ ├── another │ │ └── .git-hello │ ├── macintosh.hqx │ ├── mystery-file │ └── strange.tXT └── plain.txt └── spacecookie.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | # run additionally every week to catch 10 | # breakage due to package updates 11 | schedule: 12 | - cron: '23 23 * * 0' 13 | 14 | jobs: 15 | nix-build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4.2.2 19 | - uses: cachix/install-nix-action@v31.2.0 20 | with: 21 | nix_path: nixpkgs=channel:nixos-unstable 22 | - uses: cachix/cachix-action@v16 23 | with: 24 | name: spacecookie 25 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 26 | - name: nix-build 27 | run: nix-build 28 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy Documentation" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | doc: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4.2.2 12 | - uses: cachix/install-nix-action@v31.2.0 13 | with: 14 | nix_path: nixpkgs=channel:nixos-unstable 15 | - uses: cachix/cachix-action@v16 16 | with: 17 | name: spacecookie 18 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 19 | - name: Build web output 20 | run: | 21 | nix-build -A deploy docs/web.nix \ 22 | --option substituters 'https://cache.tvl.su https://cache.nixos.org' \ 23 | --option trusted-public-keys 'cache.tvl.su:kjc6KOMupXc1vHVufJUoDUYeLzbwSr9abcAKdn/U1Jk= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=' 24 | ./result -w tmp 25 | - name: Deploy output to GitHub Pages 26 | uses: JamesIves/github-pages-deploy-action@v4.7.3 27 | with: 28 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 29 | BRANCH: gh-pages 30 | CLEAN: true 31 | FOLDER: tmp 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Haskell ### 2 | dist 3 | dist-* 4 | cabal-dev 5 | *.o 6 | *.hi 7 | *.chi 8 | *.chs.h 9 | *.dyn_o 10 | *.dyn_hi 11 | .hpc 12 | .hsenv 13 | .cabal-sandbox/ 14 | cabal.sandbox.config 15 | *.prof 16 | *.aux 17 | *.hp 18 | *.eventlog 19 | .stack-work/ 20 | cabal.project.local 21 | cabal.project.local~ 22 | .HTF/ 23 | .ghc.environment.* 24 | /.policeman-evidence 25 | /.hie 26 | 27 | # nix 28 | result* 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision history for spacecookie 2 | 3 | ## 1.1.0.0 4 | 5 | TBD 6 | 7 | * **API BREAKING CHANGE**: Remove `Network.Gopher.Util`. 8 | Previous users of these utilities are encouraged to copy the utilities 9 | from 1.0.0.3 into their own code and adapt them to their needs. 10 | * Fix crash on malformed port values when parsing a gophermap using 11 | `Network.Gopher.Util.Gophermap`. 12 | * Fix crashes if encoding assumptions are violated in `GopherLogStr` 13 | when converting to `String` or the `Text` types. 14 | 15 | ## 1.0.0.3 16 | 17 | 2025-05-03 18 | 19 | **Security fix**: 20 | Resolve `sanitizePath` not eliminating `..` from paths. This affects users 21 | of `sanitizePath` and `sanitizePathIfNotUrl` from `Network.Gopher.Util`. 22 | 23 | This issue only affects the spacecookie library, not the spacecookie server 24 | daemon since a separate check would prevent it from handling such malicious 25 | requests (which delayed the discovery of this bug). It is probably wise to 26 | upgrade either way. 27 | 28 | Note that gophermap parsing behavior is unchanged, i.e. it just `normalise`s 29 | paths, even though `makeGophermapFilePath` used to call `sanitizePath` in 30 | previous versions. This is due to the assumption that gophermaps come from a 31 | trusted source and/or paths produced from gophermap parsing aren't used to 32 | access files directly, i.e. those paths are only served to clients (whose later 33 | requests are subject to selector sanitization) as selectors in menus. If those 34 | assumptions don't hold for your code, you will need to further sanitize the 35 | paths returned from `gophermapToDirectoryResponse`. 36 | 37 | ## 1.0.0.2 38 | 39 | 2022-10-03 40 | 41 | * Work around [cabal#8458](https://github.com/haskell/cabal/issues/8458), 42 | ensuring that the test suite can be compiled with `cabal-install` 3.8.1.0. 43 | * Always compile test suite with `-threaded` to avoid random 44 | `CurlBadFunctionArg` exceptions when executing integration tests. 45 | 46 | ## 1.0.0.1 47 | 48 | 2021-11-29 49 | 50 | This release fixes compilation with `aeson >= 2.0`. 51 | 52 | ## 1.0.0.0 53 | 54 | 2021-03-16 55 | 56 | TL;DR: 57 | 58 | * For server users, new features and configuration options have been 59 | added, but old configuration stays compatible. However some gophermap 60 | files may need adjusting, especially if they contain absolute paths 61 | not starting with a slash. 62 | * For library users there are multiple breaking changes to the core API 63 | that probably need adjusting in downstream usage as well as some 64 | changes to behavior. 65 | 66 | ### Server and Library Users 67 | 68 | #### Gophermap parsing 69 | 70 | There have been quite a few, partly breaking changes to gophermap parsing in 71 | the library in an effort to fully support the format used in pygopherd and 72 | bucktooth. Instances where spacecookie's parsing deviated from the established 73 | format have been resolved and we now ship a test suite which checks compliance 74 | against sample files from bucktooth and pygopherd. 75 | 76 | We now support relative paths correctly: If a selector in a gophermap doesn't 77 | start with `/` or `URL:` it is interpreted as a relative path and prefixed 78 | with the directory the gophermap is located in. This should make writing 79 | gophermaps much more convenient, as it isn't necessary to enter absolute 80 | selectors anymore. However, absolute selectors not starting with `/` 81 | are **broken** by this. 82 | 83 | To facilitate these changes, the API of `Network.Gopher.Util.Gophermap` 84 | changed in the following way: 85 | 86 | * `GophermapEntry` changed to use `GophermapFilePath` instead of `FilePath` 87 | which may either be `GophermapAbsolute`, `GophermapRelative` or `GophermapUrl`. 88 | Additionally, `GophermapFilePath` is a wrapper around `RawFilePath`, contrary 89 | to the previous use of `FilePath`. 90 | * `gophermapToDirectoryResponse` takes an additional parameter describing 91 | the directory the gophermap is located in to resolve relative to absolute 92 | selectors. 93 | 94 | See also [#22](https://github.com/sternenseemann/spacecookie/issues/22) and 95 | [#23](https://github.com/sternenseemann/spacecookie/pull/23). 96 | 97 | Menu lines which only contain a file type and name are now required to be 98 | terminated by a tab before the newline. This also reflects the behavior 99 | of bucktooth and pygopherd (although the latter's documentation on this 100 | is a bit misleading). Although this **breaks** entries like `0/file`, 101 | info lines which start with a valid file type character like 102 | `1. foo bar baz` no longer get mistaken for normal menu entries. 103 | See [#34](https://github.com/sternenseemann/spacecookie/pull/34). 104 | 105 | The remaining, less significant changes are: 106 | 107 | * Fixed parsing of gophermap files whose last line isn't terminated 108 | by a newline. 109 | * The `gophermaplineWithoutFileTypeChar` line type which mapped menu entries 110 | with incompatible file type characters to info lines has been removed. Such 111 | lines now result in a parse error. This is a **breaking change** if you 112 | relied on this behavior. 113 | * `parseGophermap` now consumes the end of input. 114 | 115 | #### Changes to Connection Handling 116 | 117 | * We now wait up to 1 second for the client to close the connection on 118 | their side after sending all data. This fixes an issue specific to 119 | `curl` which would result in it failing with a recv error (exit code 120 | 56) randomly. 121 | See also [#42](https://github.com/sternenseemann/spacecookie/issues/42) 122 | and [#44](https://github.com/sternenseemann/spacecookie/pull/44). 123 | * Requests from clients are now checked more vigorously and limited 124 | in size and time to prevent denial of service attacks. 125 | * Requests may not exceed 1MB in size 126 | * The client is only given 10s to send its request 127 | * After the `\r\n` no additional data may be sent 128 | 129 | ### Server Users 130 | 131 | #### Configuration 132 | 133 | * Add new `listen` field to configuration allowing to specify the 134 | listening address and port. It expects an object with the fields 135 | `port` and `addr`. The top level `port` option has been *deprecated* 136 | as a result. It is now possible to bind to the link local address 137 | `::1` only without listening on public addresses. 138 | See [#13](https://github.com/sternenseemann/spacecookie/issues/13) and 139 | [#19](https://github.com/sternenseemann/spacecookie/pull/19). 140 | * Log output is now configurable via the new `log` field in the 141 | configuration. Like `listen` it expects an object which supports the 142 | following fields. 143 | See [#10](https://github.com/sternenseemann/spacecookie/issues/10) and 144 | [#20](https://github.com/sternenseemann/spacecookie/pull/20). 145 | * `enable` allows to enable and disable logging 146 | * `hide-ips` can be used to hide private information of users from 147 | log output. This is *now enabled by default*. 148 | * `hide-time` allows to hide timestamps if your log setup already 149 | takes care of that. 150 | * `level` allows to switch between `error` and `info` log level. 151 | * Make `port` and `listen` → `port` settings optional, defaulting to 70. 152 | 153 | Config parsing should be backwards compatible. Please open a bug report if 154 | you experience any problems with that or any constellation of the new 155 | settings. 156 | 157 | #### Other changes 158 | 159 | * A not allowed error is now generated if there are any dot directories or 160 | dot files along the path: `/foo/.dot/bar` would now generate an error 161 | instead of being processed like before. 162 | * GHC RTS options are now enabled and the default option `-I10` is passed to 163 | spacecookie. 164 | * Exit if dropping privileges fails instead of just logging an error like before. 165 | See [#45](https://github.com/sternenseemann/spacecookie/pull/45). 166 | * Expand user documentation by adding three man pages 167 | ([rendered](https://sternenseemann.github.io/spacecookie/)) on the server daemon: 168 | * `spacecookie(1)`: daemon invocation and behavior 169 | * `spacecookie.json(5)`: daemon configuration 170 | * `spacecookie.gophermap(5)`: gophermap format documentation 171 | * Fix the file not found error message erroneously stating that access of that 172 | file was not permitted. 173 | * Clarify error message when an URL: selector is sent to spacecookie. 174 | * Print version when `--version` is given 175 | * Print simple usage instructions when `--help` is given or the command line 176 | can't be parsed. 177 | * A warning is now logged when a gophermap file doesn't parse and the standard 178 | directory response is used as fallback. 179 | 180 | ### Library Users 181 | 182 | ### New Representation of Request and Response 183 | 184 | The following changes are the most significant to the library as they 185 | break virtually all downstream usage of spacecookie as a library. 186 | 187 | The gopher request handler for the `runGopher`-variants now receives 188 | a `GopherRequest` record representing the request instead of the 189 | selector as a `String`. The upsides of this are as follows: 190 | 191 | * Handlers now know the IPv6 address of the client in question 192 | * Simple support for search transaction is introduced as the request 193 | sent by the client is split into selector and search string. 194 | * Selectors are no longer required to be UTF-8 as `ByteString` is used. 195 | 196 | If you want to reuse old handlers with minimal adjustments you can 197 | use a snippet like the following. Note though that you might have 198 | to make additional adjustments due to the changes to responses. 199 | 200 | wrapLegacyHandler :: (String -> GopherResponse) 201 | -> (GopherRequest -> GopherResponse) 202 | wrapLegacyHandler f = f . uDecode . requestSelectorRaw 203 | 204 | Corresponding to the switch to `ByteString` in `GopherRequest` the 205 | whole API now uses `ByteString` to represent paths and selectors. 206 | This prompts the following additional, breaking changes: 207 | 208 | * `ErrorResponse` now uses a `ByteString` instead of a `String`. 209 | * `GopherMenuItem`'s `Item` now uses a `ByteString` instead of a `FilePath` 210 | (you can use `encodeFilePath` from `filepath-bytestring` to fix downstream 211 | usage). 212 | * `sanitizePath` and `sanitizeIfNotUrl` now operate on `RawFilePath`s 213 | (which is an alias for `ByteString`). 214 | * As already mentioned, the gophermap API uses `RawFilePath`s instead 215 | of `FilePath`s as well. 216 | 217 | See also [#38](https://github.com/sternenseemann/spacecookie/pull/38) 218 | and [#26](https://github.com/sternenseemann/spacecookie/issues/26). 219 | 220 | #### Logging 221 | 222 | The built-in logging support has been removed in favor of a log handler the 223 | user can specify in `GopherConfig`. This is a **breaking change** in two ways: 224 | 225 | * The type of `GopherConfig` changed as it has a new field called 226 | `cLogHandler`. 227 | * By default (`defaultGopherConfig`) the spacecookie library no longer 228 | has logging enabled. 229 | 230 | The motivation for this was to enable the library user to influence the log 231 | output more. More specifically the following abilities were to be made 232 | possible for the bundled server daemon: 233 | 234 | * It should be possible to hide timestamps in the log output: If you are 235 | using systemd for example, the journal will take care of those. 236 | * There should be the ability to hide sensitive information from the log 237 | output: Unless necessary client IP addresses shouldn't be logged to 238 | disk. 239 | * The log output should be filterable by log level. 240 | * It should be easy for server implementation to also output log messages 241 | via the same system as the `spacecookie` library. 242 | 243 | The best solution to guarantee these properties (and virtually any you could 244 | want) is to let the library user implement logging. This allows any target 245 | output, any kind of logging, any kind of clock interaction to generate 246 | timestamps (or not) etc. This is why the spacecookie library no longer 247 | implements logging. Instead it lets you configure a `GopherLogHandler` 248 | which may also be used by the user application (it is a simple `IO` 249 | action). This additionally scales well: In the simplest case this could 250 | be a trivial wrapper around `putStrLn`. 251 | 252 | The second part to the solution is `GopherLogStr` which is the string type 253 | given to the handler. Internally this is currently implemented as a `Seq` 254 | containing chunks of `Builder`s which are coupled with meta data. This 255 | should allow decent performance in building and rendering of `GopherLogStr`s. 256 | The latter of which is relatively convenient using `FromGopherLogStr`. 257 | 258 | The tagged chunks are used to allow a clean implementation of hiding sensitive 259 | data: `makeSensitive` can be used to tag all chunks of a `GopherLogStr` which 260 | will then be picked up by `hideSensitive` which replaces all those chunks 261 | with `[redacted]`. This way sensitive information can be contained inline in 262 | strings and users can choose at any given point whether it should remain there 263 | or be hidden. 264 | 265 | The new logging mechanism was implemented in 266 | [#29](https://github.com/sternenseemann/spacecookie/pull/29). 267 | 268 | Previously it was attempted to make built-in logging more configurable 269 | (see [#13](https://github.com/sternenseemann/spacecookie/issues/13) and 270 | [#19](https://github.com/sternenseemann/spacecookie/pull/19)), but this 271 | was overly complicated and not as flexible as the new solution. Therefore 272 | it was scrapped in favor of the new system. 273 | 274 | #### Other Changes 275 | 276 | * `cRunUserName` has been removed from `GopherConfig` since the functionality 277 | doesn't need special treatment as users can implement it easily via the 278 | ready action of `runGopherManual`. The formerly internal `dropPrivileges` 279 | function is now available via `Network.Gopher.Util` to be used for this 280 | purpose. See [#45](https://github.com/sternenseemann/spacecookie/pull/45). 281 | This is a **breaking change** and requires adjustment if you used the built 282 | in privilege deescalation capabilities. 283 | * `santinizePath` and `santinizeIfNotUrl` have been corrected to `sanitizePath` 284 | and `sanitizeIfNotUrl` respectively. This is a **breaking change** to the 285 | interface of `Network.Gopher.Util`. 286 | 287 | ## 0.2.1.2 Bump fast-logger 288 | 289 | 2020-05-23 290 | 291 | * Bump fast-logger dependency, fix build 292 | 293 | ## 0.2.1.1 Fixed Privilege Dropping 294 | 295 | 2019-12-10 296 | 297 | * Server 298 | * Make `user` parameter in config optional. If it is not given or set to `null`, `spacecookie` won't attempt 299 | to change its UID and GID. This is especially useful, if socket activation is used. In that case it is not 300 | necessary to start spacecookie as `root` since systemd sets up the socket, so `spacecookie` can be already 301 | started by the right user and doesn't need to change UID. 302 | * Example Systemd config files 303 | * `SocketMode` is now `660` instead of default `666`. 304 | * Set `User` and `Group` for `spacecookie.service` as well. 305 | * Set `"user": null` in `spacecookie.json` 306 | * Library 307 | * Fixed issue that led to `runGopher*` trying to change UID even if it wasn't possible (not running as root). 308 | This especially affected the `spacecookie` server, since `cRunUserName` would always be `Just`. 309 | * Made logging related to `dropPrivileges` clearer. 310 | 311 | ## 0.2.1.0 Systemd Support 312 | 313 | 2019-10-20 314 | 315 | * Improved systemd support. 316 | * Support for the notify service type 317 | * Support for socket activation and socket (fd) storage 318 | * To make use of these new features you'll have to update your service files 319 | * Added `defaultConfig` value to prevent future breakage in software using the 320 | library when the `GopherConfig` type is extended. 321 | * Pretty print IPv6 addresses in logging 322 | 323 | ## 0.2.0.1 Hackage release 324 | 325 | 2019-05-23 326 | 327 | Fixed a problem hindering the hackage release. 328 | 329 | ## 0.2.0.0 initial release 330 | 331 | 2019-05-23 332 | 333 | * First version. Released on an unsuspecting world. Includes: 334 | * Library for writing any gopher server / application. 335 | * File system based gopher server with support for gopher maps. 336 | * Supports logging, privilege dropping, the gopher protocol and common extensions. 337 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spacecookie 2 | 3 | Haskell gopher server daemon and library. 4 | 5 | ## Status 6 | 7 | * The author doesn't run spacecookie in production (anymore). 8 | In this sense it can be considered unproven software. 9 | * A known issue is that spacecookie [doesn't use text file 10 | transactions for ASCII files](https://github.com/sternenseemann/spacecookie/issues/46). 11 | which works well with all tested clients. It should 12 | still be fixed, but it is unclear what the best approach 13 | for detecting the correct transaction type would be. 14 | * Development is essentially in maintenance mode. 15 | 16 | ## Features 17 | 18 | * (mostly) implements RFC1436 19 | * optionally supports common protocol extensions: 20 | * informational entries via the `i`-type 21 | * [`h`-type and URL entries](http://gopher.quux.org:70/Archives/Mailing%20Lists/gopher/gopher.2002-02%7C/MBOX-MESSAGE/34) 22 | * supports gophermaps (see [below](#adding-content)) 23 | * supports systemd socket activation 24 | * provides a library for custom gopher applications ([see documentation](http://hackage.haskell.org/package/spacecookie/docs/Network-Gopher.html)) 25 | 26 | ## Non-Features 27 | 28 | spacecookie intentionally does not support: 29 | 30 | * HTTP, Gemini: Multi protocol support is a non-goal for spacecookie. 31 | For HTTP you can [proxy](https://github.com/sternenseemann/gopher-proxy) 32 | pretty easily, however. 33 | * Search: Gopher supports search transactions, but the spacecookie daemon doesn't offer 34 | the possibility to add a search engine to a gopherspace. It is however 35 | entirely possible to implement an index search server using [the 36 | spacecookie library](https://hackage.haskell.org/package/spacecookie/docs/Network-Gopher.html) 37 | 38 | ## Installation 39 | 40 | * Nix(OS): [`pkgs.haskellPackages.spacecookie`](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&query=spacecookie) 41 | (see also [below](#on-nixos)) 42 | * Cabal: `cabal v2-install spacecookie` 43 | (see also [hackage package](http://hackage.haskell.org/package/spacecookie)) 44 | 45 | ## Documentation 46 | 47 | * User Documentation: [spacecookie(1)](https://sternenseemann.github.io/spacecookie/spacecookie.1.html) 48 | * [Developer Documentation](https://hackage.haskell.org/package/spacecookie) 49 | 50 | ## Configuration 51 | 52 | spacecookie is configured via a JSON configuration file. 53 | All available options are documented in 54 | [spacecookie.json(5)](https://sternenseemann.github.io/spacecookie/spacecookie.json.5.html). 55 | This repository also contains an example configuration file in 56 | [`etc/spacecookie.json`](./etc/spacecookie.json). 57 | 58 | ## Running 59 | 60 | After you've created your config file just start spacecookie like this: 61 | 62 | spacecookie /path/to/spacecookie.json 63 | 64 | spacecookie runs as a simple process and doesn't fork or write a PID file. 65 | Therefore any supervisor (systemd, daemontools, ...) can be used to run 66 | it as a daemon. 67 | 68 | ### With systemd 69 | 70 | spacecookie supports systemd socket activation. To set it up you'll need 71 | to install `spacecookie.service` and `spacecookie.socket` like so: 72 | 73 | cp ./etc/spacecookie.{service,socket} /etc/systemd/system/ 74 | systemctl daemon-reload 75 | systemctl enable spacecookie.socket 76 | systemctl start spacecookie.socket 77 | systemctl start spacecookie.service # optional, started by the socket automatically if needed 78 | 79 | Of course make sure that all the used paths are correct! 80 | 81 | How the systemd integration works is explained in 82 | [spacecookie(1)](https://sternenseemann.github.io/spacecookie/spacecookie.1.html#SYSTEMD_INTEGRATION). 83 | 84 | ### On NixOS 85 | 86 | [NixOS](https://nixos.org/nixos/) provides a service module for spacecookie: 87 | [`services.spacecookie`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/networking/spacecookie.nix). 88 | Setting up spacecookie is as simple as adding the following line to your `configuration.nix`: 89 | 90 | services.spacecookie.enable = true; 91 | 92 | For all available options, refer to the NixOS manual: 93 | 94 | * [NixOS stable](https://nixos.org/manual/nixos/stable/options.html#opt-services.spacecookie.enable) 95 | * [NixOS unstable](https://nixos.org/manual/nixos/unstable/options.html#opt-services.spacecookie.enable) 96 | 97 | ## Adding Content 98 | 99 | spacecookie acts as a simple file server, only excluding files 100 | or directories that start with a dot. It generates gopher menus 101 | automatically, but you can also use custom ones by adding a 102 | gophermap file. 103 | 104 | spacecookie checks for `.gophermap` in every directory it serves and, 105 | if present, uses the menu specified in there. 106 | 107 | Such a file looks like this: 108 | 109 | You can just start writing text that 110 | will be displayed by the gopher client 111 | without a link to a file. Empty lines are 112 | also possible. 113 | 114 | 1Menu Entry for a directory full of funny stuff /funny 115 | IFunny Image /funny.jpg 116 | gcat gif /cat.gif 117 | 0about me /about.txt 118 | 1Floodgap's gopher server / gopher.floodgap.com 70 119 | 120 | As you can see, it largely works like the actual gopher menu a server will 121 | send to clients, but allows to omit redundant information and to insert 122 | lines that are purely informational and not associated with a file. 123 | [spacecookie.gophermap(5)](https://sternenseemann.github.io/spacecookie/spacecookie.gophermap.5.html) 124 | explains syntax and semantics in more detail. 125 | 126 | The format is compatible with the ones supported by 127 | [Bucktooth](gopher://gopher.floodgap.com/1/buck/) and 128 | [pygopherd](https://github.com/jgoerzen/pygopherd). 129 | If you notice any incompatibilities, please open an issue. 130 | 131 | ## Portability 132 | 133 | spacecookie is regularly tested on GNU/Linux via CI, but 134 | should also work on other Unix-like operating systems. 135 | Most portability problems arise due to 136 | [haskell-socket](https://github.com/lpeterse/haskell-socket) 137 | which is for example known 138 | [not to work on OpenBSD](https://github.com/lpeterse/haskell-socket/issues/63). 139 | 140 | Windows support would be possible, but could be tricky as gopher 141 | expects Unix-style directory separators in paths. I personally 142 | don't want to invest time into it, but would accept patches adding 143 | Windows support. 144 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | let 4 | hl = pkgs.haskell.lib; 5 | 6 | src = builtins.path { 7 | name = "spacecookie-source"; 8 | path = ./.; 9 | filter = pkgs.nix-gitignore.gitignoreFilter 10 | (builtins.readFile ./.gitignore) ./.; 11 | }; 12 | 13 | profiled = pkgs.haskellPackages.override { 14 | overrides = self: super: { 15 | mkDerivation = args: super.mkDerivation (args // { 16 | enableLibraryProfiling = true; 17 | }); 18 | 19 | spacecookie = hl.overrideCabal 20 | (self.callPackage ./spacecookie.nix {}) 21 | (drv: { 22 | version = "unstable"; 23 | # build from sdist to make sure it isn't missing anything 24 | src = self.cabalSdist { 25 | src = ./.; 26 | name = "spacecookie-unstable-sdist.tar.gz"; 27 | }; 28 | # run integration test 29 | preCheck = '' 30 | export SPACECOOKIE_TEST_BIN=./dist/build/spacecookie/spacecookie 31 | ''; 32 | # install man pages 33 | postInstall = '' 34 | install -Dm644 docs/man/*.1 -t "$out/share/man/man1" 35 | install -Dm644 docs/man/*.5 -t "$out/share/man/man5" 36 | ''; 37 | }); 38 | }; 39 | }; 40 | 41 | in 42 | 43 | if !pkgs.lib.inNixShell 44 | then profiled.spacecookie 45 | else profiled.spacecookie.env 46 | -------------------------------------------------------------------------------- /docs/bucktooth-gophermap.txt: -------------------------------------------------------------------------------- 1 | Bucktooth: Serving files and directories, the gophermap file and gophertags 2 | --------------------------------------------------------------------------- 3 | 4 | The gophermap file is responsible for the look of a gopher menu. 5 | 6 | Unlike the UMN gopherd-style map files, which are somewhat cumbersome and 7 | can get rather large, Bucktooth encourages a slimline approach, or you can 8 | have none at all. This is not too secure since it will happily serve any and 9 | every file in its mountpoint to a greedy user, but if that's really what you 10 | want, congratulations. You can stop reading this now, since that's exactly 11 | what it will do when you install it with no gophermap files. Only gophermap, 12 | ., and .. are not served to the user. If you are using Bucktooth 0.2 and 13 | you turn on sorting, then the directory listing is alpha-sorted for you too. 14 | 15 | What if you want to give your files some sort of proper description instead of 16 | just their name? If you have Bucktooth 0.2 on and you enable smart linking, 17 | then you can make a symlink to the file with a nice long proper name, and both 18 | that and the original file will both point to the same place (the original 19 | file). Thus, you can make a proper menu out of any filesystem: any symbolic 20 | links in that directory will be automatically dereferenced for you into a 21 | consistent relative selector -- meaning that the name of the symlink will 22 | be used as the display string, but the selector will be what it *points to*, 23 | relative to the server's mountpoint. This lets you give your files some sort 24 | sort of meaningful descriptive text both in the filesystem and in gopherspace 25 | and yet still have the relationships between symlinks and their target files 26 | explicitly maintained. Smart linking is automatic and other than turning it 27 | on, you don't need to do anything else. This might be all some sites need for 28 | menus. 29 | 30 | --- DIVERSION, SKIP IF YOU DON'T CARE --- 31 | (Why have a feature like this? Smart linking allows you to have lots of 32 | symbolic links to directories in your mountpoint, but have the actual 33 | selectors remain consistent so that bots and smart clients traversing your 34 | content will Do The Right Thing(tm) and treat the symlinks as one resource 35 | instead of handling the symlinks as individual unique selectors unnecessarily, 36 | repetitively and separately. However, this feature is also smart enough to 37 | know that links outside of Bucktooth's mountpoint should be left alone or else 38 | the client won't be able to get to it. Because it doesn't restrict symlinks 39 | pointing outside of the server's mountpoint, you should not assume that smart 40 | linking is the equivalent of chroot()ing Bucktooth; running Bucktooth inside 41 | of a chroot()ed environment is considered beyond the scope of this manual. 42 | Smart linking doesn't mean "genius linking" either: if your file really 43 | points to a symlink of a symlink, or a symlink's symlink's symlink, etc., or 44 | resides in a directory tree that may be made up of symlinks to other 45 | directories, these kinds of indirect linkages are not resolved to cut down 46 | on execution time and complexity. This should be considered a feature; if 47 | explicitly stating this is important, forge an entry in the gophermap. Smart 48 | linking, however, is so socially advantageous in general for intelligent 49 | clients and users that it is the default.) 50 | --- END DIVERSION --- 51 | 52 | However, even smart linking isn't enough for many sites and it doesn't handle 53 | external links. This is where Bucktooth's gophermap files come in. Create or 54 | edit the gophermap file (one per directory) with any text editor and follow a 55 | few simple rules to gopher goodness. (A sample file is in stuff/ for your 56 | enjoyment.) 57 | 58 | Bucktooth sends any RFC-1436 compliant line to the client. In other words, 59 | 60 | 1gopher.floodgap.com homegopher.floodgap.com70 61 | 62 | where , is of course, the tab (CTRL-I, 0x09) character, generates a 63 | link to "null" selector on gopher.floodgap.com 70 with an itemtype of 1 and 64 | a display string of "gopher.floodgap.com home". You don't even have to enter 65 | valid selectors, although this will not endear you much to your users. 66 | (Null selector is the same as the root of the mountpoint, by the way.) 67 | 68 | If you are not well-versed in RFC-1436, it breaks down to the first character 69 | being the itemtype (0 = text, 1 = gopher menu, 5 = zip file, 9 = generic 70 | binary, 7 = search server, I = generic image, g = gif image, h = HTML; others 71 | are also supported by some clients), then the string shown by the client up to 72 | the first tab ("display string"); then the full path to the resource 73 | ("selector"); the hostname of the server; and the port. 74 | 75 | Since it would be a major drag to always have to type things out in full, 76 | Bucktooth allows the following shortcuts: 77 | 78 | * If you don't specify a port, Bucktooth provides the one your server is 79 | using (almost always 70). 80 | 81 | * If you don't specify a host, Bucktooth provides your server's hostname. 82 | 83 | * If you only specify a relative selector and not an absolute path, Bucktooth 84 | sticks on the path they're browsing. 85 | 86 | So, if your server is gopher.somenetwork.com and your server's port is 7070, 87 | and this gophermap is inside of /lotsa, then 88 | 89 | 1Lots of stuffstuff 90 | 91 | is expanded out to 92 | 93 | 1Lots of stuff/lotsa/stuffgopher.somenetwork.com7070 94 | 95 | If you don't specify a selector, two things can happen. Putting a at 96 | the end, like 97 | 98 | 1src 99 | 100 | explicitly tells Bucktooth you aren't specifying a selector, so Bucktooth 101 | uses your display string as the selector, adds on the host and port, and 102 | gives the client that. (If you have Bucktooth 0.2 or higher and you enable 103 | gopher globbing, then this **specific** type of gophermap entry is subject to 104 | wildcard expansion, which is highly useful. See the separate section on that.) 105 | 106 | If you enable gopher tagging, supported in Bucktooth 0.2.8 and higher, and 107 | the resource is a directory with a "gophertag" file, then the contents of 108 | that file are used as the display string instead, and the "display string" 109 | becomes the selector. This is useful for centralized resource naming. 110 | 111 | Otherwise, any lines without any characters in them are interpreted 112 | as free text descriptions and Bucktooth will give them an i itemtype to 113 | instruct the client to display them as non-interactive text. This allows you 114 | to add text descriptions to your menus (look at gopher.floodgap.com for an 115 | example). However, don't use the character anywhere in your text 116 | description or Bucktooth will try to interpret it as an RFC-1436 resource, 117 | which will yield possibly hilarious and definitely erroneous results. 118 | 119 | If you are running Bucktooth 0.2 or higher, and you make your gophermap file 120 | executable by the uid running Bucktooth, it will be treated as if it were a 121 | mole. This allows completely dynamic behaviour. See the section on moles for 122 | how executables are handled in Bucktooth. 123 | 124 | One last warning: keep display strings at 67 characters or less -- some 125 | clients may abnormally wrap them or display them in a way you didn't intend. 126 | 127 | 128 | . 129 | -------------------------------------------------------------------------------- /docs/man/spacecookie.1: -------------------------------------------------------------------------------- 1 | .Dd $Mdocdate$ 2 | .Dt SPACECOOKIE 1 3 | .Os 4 | .Sh NAME 5 | .Nm spacecookie 6 | .Nd gopher server daemon 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl -version 10 | .Ar config.json 11 | .Sh DESCRIPTION 12 | .Nm 13 | is a simple to use gopher daemon for serving static files. 14 | It is either invoked with the 15 | .Fl -version 16 | flag to print its version or with the path to its config file 17 | as the single argument. 18 | The minimal config file needs to tell 19 | .Nm 20 | about the directory to serve and the server's name, i. e. the hostname 21 | or IP address the server is reachable through. 22 | All configuration options, the format and default values are explained in 23 | .Xr spacecookie.json 5 . 24 | .Pp 25 | On startup, 26 | .Nm 27 | will check if it has been started with systemd socket activation. 28 | If that's true, it will use the socket passed from systemd, if not, 29 | it will setup the socket itself. 30 | After that it will call 31 | .Xr setuid 2 32 | to switch to a less privileged user if configured to do so and start 33 | accepting incoming gopher requests on the socket. 34 | Note that using socket activation eliminates the need for starting 35 | as a privileged user in the first place because systemd will take 36 | care of the socket. 37 | The systemd integration is explained in more detail in its own section. 38 | .Pp 39 | .Nm 40 | will not fork itself to the background or otherwise daemonize 41 | which can, however, be achieved using a supervisor. 42 | Logs are always written to 43 | .Sy stderr 44 | and can be collected and rotated by another daemon or tool if desired. 45 | .Pp 46 | Incoming requests are filtered: No files or directories outside 47 | the served directory or that start with a dot may be accessed by clients. 48 | Allowed files are returned to clients unfiltered. For directories, 49 | .Nm 50 | checks if they contain a 51 | .Ql .gophermap 52 | file: If they contain one, it is used to generate the directory response, 53 | otherwise one is generated automatically which involves guessing all file 54 | types from file extensions. 55 | The default file type is 56 | .Ql 0 , 57 | text file. 58 | The file format of the 59 | .Ql gophermap 60 | files and its use are explained in 61 | .Xr spacecookie.gophermap 5 . 62 | .Sh SYSTEMD INTEGRATION 63 | .Nm 64 | optionally supports two systemd-specific features: 65 | It acts as a 66 | .Sy notify 67 | type service and supports socket activation. 68 | .Pp 69 | If you are writing a 70 | .Xr systemd.service 5 71 | file, be sure to use the 72 | .Ql Type=notify 73 | directive which allows 74 | .Nm 75 | to tell systemd when it has finished starting up and 76 | when it is stopping before actually exiting. 77 | .Pp 78 | For socket activation, create a 79 | .Xr systemd.socket 5 80 | file that starts the 81 | .Nm 82 | service. 83 | This has several advantages: For one, it allows starting 84 | .Nm 85 | on demand only and reducing the load on server startup. 86 | Additionally it means that the daemon doesn't ever need 87 | to be started as root because it won't need to setup a 88 | socket bound to a well-known port. 89 | .Pp 90 | Mind the following points when configuring socket activation: 91 | .Bl -bullet 92 | .It 93 | The port set in the 94 | .Xr systemd.socket 5 95 | file must match the port configured in 96 | .Xr spacecookie.json 5 . 97 | .It 98 | The socket set up by 99 | .Xr systemd 1 100 | must use the IPv6 address family and the TCP protocol. 101 | It is recommended to always set 102 | .Ql BindIPv6Only=both 103 | in 104 | .Xr systemd.socket 5 . 105 | To listen on an IPv4 address only, you can use an IPv6 socket 106 | with a mapped IPv4 address. 107 | .It 108 | As always the 109 | .Sy hostname 110 | setting must match the public address or hostname the socket is listening on. 111 | .El 112 | .Pp 113 | Make sure to check your socket configuration settings carefully since 114 | .Nm 115 | doesn't run any sanity checks on the socket received from 116 | .Xr systemd 1 117 | yet. 118 | .Pp 119 | An example 120 | .Xr systemd.service 5 121 | and 122 | .Xr systemd.socket 5 123 | file are provided in the 124 | .Nm 125 | source distribution in the 126 | .Ql etc 127 | directory. 128 | .Sh SEE ALSO 129 | .Xr spacecookie.json 5 , 130 | .Xr spacecookie.gophermap 5 , 131 | .Xr systemd.service 5 132 | and 133 | .Xr systemd.socket 5 . 134 | .Pp 135 | For writing custom gopher application using the spacecookie library refer to the 136 | .Lk https://hackage.haskell.org/package/spacecookie API documentation . 137 | .Sh STANDARDS 138 | By default, 139 | .Nm 140 | always behaves like a gopher server as described in 141 | .Lk https://tools.ietf.org/html/rfc1436 RFC1436 . 142 | However users can configure 143 | .Nm 144 | to utilize common protocol extensions like the 145 | .Ql h 146 | and 147 | .Ql i 148 | types and 149 | .Lk http://gopher.quux.org:70/Archives/Mailing%20Lists/gopher/gopher.2002-02%7C/MBOX-MESSAGE/34 URLs to other protocols . 150 | .Sh AUTHORS 151 | .Nm 152 | has been written and documented by 153 | .An sternenseemann , 154 | .Mt sterni-spacecookie@systemli.org . 155 | .Sh SECURITY CONSIDERATIONS 156 | .Nm 157 | supports no migitations or attack surface reduction measures other than 158 | automatically switching to a less privileged user after binding. 159 | It is recommended to use this feature and to make use of containering 160 | or sandboxing like for example 161 | .Xr systemd.exec 5 162 | supports. 163 | .Pp 164 | TLS-enabled gopher, like the 165 | .Ql gophers 166 | protocol supported by 167 | .Xr curl 1 168 | is not natively supported by 169 | .Nm 170 | at this time. 171 | -------------------------------------------------------------------------------- /docs/man/spacecookie.gophermap.5: -------------------------------------------------------------------------------- 1 | .Dd $Mdocdate$ 2 | .Dt SPACECOOKIE.GOPHERMAP 5 3 | .Os 4 | .Sh NAME 5 | .Nm spacecookie.gophermap 6 | .Nd gophermap file format supported by 7 | .Xr spacecookie 1 8 | .Sh DESCRIPTION 9 | A gophermap file allows to describe a gopher menu without the need to include redundant information. 10 | The format supported by 11 | .Xr spacecookie 1 12 | has originally been introduced by Bucktooth and is supported by most popular gopher server daemons like for example 13 | .Xr pygopherd 8 . 14 | .Pp 15 | A gophermap file stored as 16 | .Ql .gophermap 17 | in a directory under the gopher root of 18 | .Xr spacecookie 1 19 | is parsed and used as a gopher menu instead of the automatically generated default variant. 20 | This allows users to customize the directory listings by specifying informational text, 21 | links to files, (other) directories, gopher servers or protocols themselves. 22 | .Sh FORMAT 23 | The format is plain text and line based. Both Unix and DOS style line endings are allowed. 24 | .Xr spacecookie 1 25 | distinguishes between two types of lines: 26 | .Bl -tag -width 4n 27 | .It Sy info lines 28 | Info lines are lines of text in a gophermap which don't have any special 29 | format requirements except that they may not contain any tab characters. 30 | .Pp 31 | .Dl Any text which may contain anything but tabs. 32 | .Pp 33 | They are also rendered als plain text without any associated links to gopher 34 | clients which support them. Info lines are technically not part of the gopher 35 | protocol nor mentioned in RFC1436, but this protocol extension is 36 | widely supported and used. 37 | .Pp 38 | The usual purpose is to display additional text, headings and decorative elements 39 | which are not directly related to other resources served via gopher: 40 | .Bd -literal -offset indent 41 | +------------------------------+ 42 | | Welcome to my Gopher Server! | 43 | +------------------------------+ 44 | 45 | Below you can find a collection of files I deemed 46 | interesting or useful enough to publish them. 47 | .Ed 48 | .Pp 49 | Empty lines are interpreted as info lines which have no content. 50 | .It Sy menu entries 51 | Lines describing menu entries are of the following form. 52 | All spaces are for readability only and must not be present in the actual format. 53 | Everything in brackets may be omitted, the semantics of which are explained below. 54 | .Pp 55 | .Dl gopherfiletypeNAME\\\\t Op SELECTOR Op \\\\tSERVER Op \\\\tPORT 56 | .Bl -tag -width 1n 57 | .It Em gopherfiletype 58 | File type character indicating the file type of the linked resource to the client. 59 | See 60 | .Lk https://tools.ietf.org/html/rfc1436#page-14 RFC1436 61 | for a list of valid file types. 62 | Additionally, 63 | .Xr spacecookie 1 64 | supports 65 | .Ql i 66 | which indicates an info line and 67 | .Ql h 68 | which indicates an HTML document. 69 | .It Em NAME 70 | Name of the linked resource which will show up as the text of the menu entry. 71 | May contain any characters except newlines and tabs. 72 | .Em NAME 73 | must always be terminated by a tab. 74 | .It Em SELECTOR 75 | Gopher selector the entry should link to. 76 | Same restrictions in terms of characters apply as for 77 | .Em NAME , 78 | but there should only be a tab character afterwards if another field is specified. 79 | If it is omitted, the value of 80 | .Em NAME 81 | is used. 82 | If the 83 | .Em SELECTOR 84 | starts with 85 | .Ql / , 86 | it is interpreted as an absolute path and given to the client as-is. 87 | If it starts with 88 | .Ql URL: , 89 | it is assumed that it is a link to another protocol and passed to the 90 | client without modification (see below). In all other cases, 91 | it is assumed that the selector is a relative path and is converted to 92 | an absolute path before serving the menu to a client. 93 | .Pp 94 | You can read more about 95 | .Ql URL: 96 | links which are another common gopher protocol extension in 97 | .Lk http://gopher.quux.org:70/Archives/Mailing%20Lists/gopher/gopher.2002-02%7C/MBOX-MESSAGE/34 this email from John Goerzen. 98 | .It Em SERVER 99 | Describes the server 100 | .Em SELECTOR 101 | should be retrieved from. 102 | Same character restrictions apply and it must come after a tab character as well. 103 | If it is omitted, the hostname of the server generating the menu is used. 104 | .It Em PORT 105 | Describes the port 106 | .Em SERVER 107 | is running on. 108 | Must come after a tab and is terminated by the end of the line or file. 109 | If this field is left out, the server generating the menu uses its own port. 110 | .El 111 | .El 112 | 113 | A gophermap file may contain any number of menu and info lines. 114 | They are then converted to actual gopher protocol menu entries clients 115 | understand line by line as described above. 116 | .Sh EXAMPLE 117 | Tabs are marked with 118 | .Ql ^I 119 | for clarity. 120 | .Bd -literal -offset indent 121 | spacecookie 122 | =========== 123 | 124 | Welcome to spacecookie's gopher page! 125 | 126 | Get a copy either by downloading the latest 127 | stable release or cloning the development version: 128 | 129 | hGitHub page^I URL:https://github.com/sternenseemann/spacecookie/ 130 | 9latest tarball^I /software/releases/spacecookie-0.3.0.0.tar.gz 131 | 132 | The following documentation resources should get you started: 133 | 134 | 0README^I README.md 135 | 1man pages^I manpages/ 136 | 137 | Other gopher server daemons (the first link only works 138 | if this server is running on port 70): 139 | 140 | 1pygopherd^I /devel/gopher/pygopherd^I gopher.quux.org 141 | 1Bucktooth^I /buck^I gopher.floodgap.com^I 70 142 | .El 143 | .Sh SEE ALSO 144 | .Xr pygopherd 8 , 145 | .Lk gopher://gopher.floodgap.com/0/buck/dbrowse?faquse%201a Bucktooth's gophermap documentation 146 | and 147 | .Lk https://tools.ietf.org/html/rfc1436#page-14 the file type list from RFC1436 . 148 | .Pp 149 | .Xr spacecookie 1 , 150 | .Xr spacecookie.json 5 151 | .Sh AUTHORS 152 | The 153 | .Nm 154 | documentation has been written by 155 | .An sternenseemann , 156 | .Mt sterni-spacecookie@systemli.org . 157 | -------------------------------------------------------------------------------- /docs/man/spacecookie.json.5: -------------------------------------------------------------------------------- 1 | .Dd $Mdocdate$ 2 | .Dt SPACECOOKIE.JSON 5 3 | .Os 4 | .Sh NAME 5 | .Nm spacecookie.json 6 | .Nd configuration file for 7 | .Xr spacecookie 1 8 | .Sh DESCRIPTION 9 | The 10 | .Xr spacecookie 1 11 | config file is a JSON file which contains a single object. 12 | The allowed fields representing individual settings and their effect are explained below. 13 | .Ss REQUIRED SETTINGS 14 | The following settings must be part of every configuration file as there 15 | is no default or fallback value for them. 16 | .Bl -tag -width 2n -offset 0n 17 | .It Sy hostname 18 | Describes the public server name 19 | .Xr spacecookie 1 20 | is reachable through, i. e. the address clients will use to connect to it. 21 | It will be used to populate gopher menus with the correct server name, so 22 | follow up requests from clients actually reach the correct server. 23 | For testing purposes, it can be useful to set it to 24 | .Ql localhost . 25 | .Pp 26 | Type: string. 27 | .It Sy root 28 | Sets the the directory 29 | .Xr spacecookie 1 30 | should serve via gopher. 31 | All gopher requests will be resolved to files or directories under that root. 32 | Files and directories will be served to users if no component of the resolved 33 | path starts with a dot and they are readable for the user 34 | .Xr spacecookie 1 35 | is running as. 36 | .Pp 37 | Type: string. 38 | .El 39 | .Ss OPTIONAL SETTINGS 40 | The following settings are optional, meaning there is either a default value 41 | or an obvious default behavior if they are not given. 42 | .Bl -tag -width 2n -offset 0n 43 | .It Sy listen 44 | Describes the address and port 45 | .Xr spacecookie 1 46 | should listen on. 47 | Both aspects can be controlled individually by the two optional fields 48 | described below. 49 | .Pp 50 | Type: object. 51 | .Bl -tag -offset 0n -width 2n 52 | .It Sy port 53 | Port to listen on. 54 | The well-known port for gopher is 55 | .Ms 70 . 56 | .Pp 57 | If 58 | .Xr systemd.socket 5 59 | activation is used, this setting will have no effect on the actual 60 | port the socket is bound to since this is done by 61 | .Xr systemd 1 . 62 | It will then only be used to display the server's port in gopher menus for 63 | subsequent requests, so make sure whatever is set here matches what 64 | .Xr systemd 1 65 | is doing. 66 | .Pp 67 | Type: number. 68 | Default: 69 | .Ql 70 . 70 | .It Sy addr 71 | Address to listen and accept gopher requests on. 72 | In contrast to 73 | .Sy hostname , 74 | this option controls the socket setup and not what is used in gopher menus. 75 | This option is especially useful to limit the addresses 76 | .Xr spacecookie 1 77 | will listen on since it listens on all available addresses 78 | for incoming requests by default, i. e. 79 | .Sy INADDR_ANY . 80 | For example, 81 | .Ql ::1 82 | can be used to listen on the link-local addresses only 83 | which comes in handy if you are setting up a onion service using 84 | .Xr tor 1 85 | and want to avoid leaking the server's identity. 86 | .Pp 87 | When given, 88 | .Xr getaddrinfo 3 89 | is used to resolve the given hostname or parse the given IP address and 90 | .Xr spacecookie 1 91 | will only listen on the resulting address(es). 92 | Note that 93 | .Sy IPV6_V6ONLY 94 | is always disabled, so, if possible, both the resulting v4 and v6 address will be used. 95 | .Pp 96 | If 97 | .Xr systemd.socket 5 98 | activation is used, this setting has no effect. 99 | .Pp 100 | Type: string. 101 | .El 102 | .It Sy user 103 | The name of the user spacecookie should run as. 104 | When this option is given and not 105 | .Ql null , 106 | .Xr spacecookie 1 107 | will call 108 | .Xr setuid 2 109 | and 110 | .Xr setgid 2 111 | after setting up its socket to switch to that user and their primary group. 112 | Note that this is only necessary to set if 113 | .Xr spacecookie 1 114 | is started with root privileges in the first place as the binary shouldn't have 115 | the setuid bit set. 116 | An alternative to starting the daemon as root, so it can bind its socket to a 117 | well-known port, is to use 118 | .Xr systemd 1 119 | socket activation. 120 | See the 121 | .Xr spacecookie 1 122 | man page for details on setting this up. 123 | .Pp 124 | Type: string. 125 | Default: 126 | .Ql null . 127 | .It Sy log 128 | Allows to customize the logging output of 129 | .Xr spacecookie 1 130 | to 131 | .Sy stderr . 132 | .Pp 133 | Type: object. 134 | .Bl -tag -offset 0n -width 2n 135 | .It Sy enable 136 | Wether to enable logging. 137 | .Pp 138 | Type: bool. 139 | Default: 140 | .Ql true . 141 | .It Sy hide-ips 142 | Wether to hide IP addresses of clients in the log output. 143 | If enabled, 144 | .Ql [redacted] 145 | is displayed instead of client's IP addresses to avoid writing personal 146 | information to disk. 147 | .Pp 148 | Type: bool. 149 | Default: 150 | .Ql true . 151 | .It Sy hide-time 152 | If this is set to 153 | .Ql true , 154 | .Xr spacecookie 1 155 | will not print timestamps at the beginning of every log line. 156 | This is useful if you use an additional daemon or tool to take care of logs 157 | which records timestamps automatically, like 158 | .Xr systemd 1 . 159 | .Pp 160 | Type: bool. 161 | Default: 162 | .Ql false . 163 | .It Sy level 164 | Controls verbosity of logging. 165 | It is recommended to either use 166 | .Qq warn 167 | or 168 | .Qq info 169 | since 170 | .Qq error 171 | hides warnings that are indicative of configuration issues. 172 | .Pp 173 | Type: either 174 | .Qq error , 175 | .Qq warn 176 | or 177 | .Qq info . 178 | Default: 179 | .Qq info . 180 | .El 181 | .El 182 | .Ss DEPRECATED SETTINGS 183 | The following settings are only supported for backwards compatibility 184 | and should be replaced in existing configurations in the way described 185 | for each respectively. 186 | .Pp 187 | .Bl -tag -width 2n -offset 0n 188 | .It Sy port 189 | The top level 190 | .Sy port 191 | is an alias for the setting of the same name inside the 192 | .Sy listen 193 | object and should be replaced by the latter. 194 | .El 195 | .Sh EXAMPLE 196 | The following configuration equates to the default behavior of 197 | .Xr spacecookie 1 198 | for all optional settings, although it is much verboser than necessary. 199 | .Bd -literal -offset Ds 200 | { 201 | "hostname" : "localhost", 202 | "root" : "/srv/gopher", 203 | "listen" : { 204 | "addr" : "::", 205 | "port" : 70 206 | }, 207 | "user" : null, 208 | "log" : { 209 | "enable" : true, 210 | "hide-ips" : true, 211 | "hide-time" : false, 212 | "level" : "info" 213 | } 214 | } 215 | .Ed 216 | .Pp 217 | This configuration is suitable for running as an onion service: 218 | It disables logging completely to not collect any kind of meta data about users 219 | and only listens on the link-local address to avoid leaking its identity. 220 | We can also use a non-well-known port since 221 | .Xr tor 1 222 | allows free mapping from local to exposed ports, so 223 | .Xr spacecookie 1 224 | can be started as a normal user. 225 | .Bd -literal -offset Ds 226 | { 227 | "hostname": "myonionservicehash.onion", 228 | "root": "/srv/onion-gopher", 229 | "listen": { 230 | "addr": "::1", 231 | "port": 7070 232 | }, 233 | "log": { 234 | "enable": false 235 | } 236 | } 237 | .Ed 238 | .Pp 239 | If you are not using socket activation for running a gopher server on the 240 | well-known port for gopher, a config like this is appropriate, provided the 241 | user 242 | .Ql gopher 243 | exists: 244 | .Bd -literal -offset Ds 245 | { 246 | "hostname": "example.org", 247 | "root": "/srv/gopher", 248 | "user": "gopher" 249 | } 250 | .Ed 251 | .Pp 252 | For a 253 | .Xr systemd.socket 5 254 | based setup, the 255 | .Ql user 256 | field should be omitted and 257 | .Xr spacecookie 1 258 | started as the target user directly in the 259 | .Xr systemd.service 5 260 | file. 261 | .Sh SEE ALSO 262 | .Xr spacecookie 1 . 263 | .Sh AUTHORS 264 | The 265 | .Nm 266 | documentation has been written by 267 | .An sternenseemann , 268 | .Mt sterni-spacecookie@systemli.org . 269 | -------------------------------------------------------------------------------- /docs/rfc1436.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group F. Anklesaria 8 | Request for Comments: 1436 M. McCahill 9 | P. Lindner 10 | D. Johnson 11 | D. Torrey 12 | B. Alberti 13 | University of Minnesota 14 | March 1993 15 | 16 | 17 | The Internet Gopher Protocol 18 | (a distributed document search and retrieval protocol) 19 | 20 | Status of this Memo 21 | 22 | This memo provides information for the Internet community. It does 23 | not specify an Internet standard. Distribution of this memo is 24 | unlimited. 25 | 26 | Abstract 27 | 28 | The Internet Gopher protocol is designed for distributed document 29 | search and retrieval. This document describes the protocol, lists 30 | some of the implementations currently available, and has an overview 31 | of how to implement new client and server applications. This 32 | document is adapted from the basic Internet Gopher protocol document 33 | first issued by the Microcomputer Center at the University of 34 | Minnesota in 1991. 35 | 36 | Introduction 37 | 38 | gopher n. 1. Any of various short tailed, burrowing mammals of the 39 | family Geomyidae, of North America. 2. (Amer. colloq.) Native or 40 | inhabitant of Minnesota: the Gopher State. 3. (Amer. colloq.) One 41 | who runs errands, does odd-jobs, fetches or delivers documents for 42 | office staff. 4. (computer tech.) software following a simple 43 | protocol for burrowing through a TCP/IP internet. 44 | 45 | The Internet Gopher protocol and software follow a client-server 46 | model. This protocol assumes a reliable data stream; TCP is assumed. 47 | Gopher servers should listen on port 70 (port 70 is assigned to 48 | Internet Gopher by IANA). Documents reside on many autonomous 49 | servers on the Internet. Users run client software on their desktop 50 | systems, connecting to a server and sending the server a selector (a 51 | line of text, which may be empty) via a TCP connection at a well- 52 | known port. The server responds with a block of text terminated by a 53 | period on a line by itself and closes the connection. No state is 54 | retained by the server. 55 | 56 | 57 | 58 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 1] 59 | 60 | RFC 1436 Gopher March 1993 61 | 62 | 63 | While documents (and services) reside on many servers, Gopher client 64 | software presents users with a hierarchy of items and directories 65 | much like a file system. The Gopher interface is designed to 66 | resemble a file system since a file system is a good model for 67 | organizing documents and services; the user sees what amounts to one 68 | big networked information system containing primarily document items, 69 | directory items, and search items (the latter allowing searches for 70 | documents across subsets of the information base). 71 | 72 | Servers return either directory lists or documents. Each item in a 73 | directory is identified by a type (the kind of object the item is), 74 | user-visible name (used to browse and select from listings), an 75 | opaque selector string (typically containing a pathname used by the 76 | destination host to locate the desired object), a host name (which 77 | host to contact to obtain this item), and an IP port number (the port 78 | at which the server process listens for connections). The user only 79 | sees the user-visible name. The client software can locate and 80 | retrieve any item by the trio of selector, hostname, and port. 81 | 82 | To use a search item, the client submits a query to a special kind of 83 | Gopher server: a search server. In this case, the client sends the 84 | selector string (if any) and the list of words to be matched. The 85 | response yields "virtual directory listings" that contain items 86 | matching the search criteria. 87 | 88 | Gopher servers and clients exist for all popular platforms. Because 89 | the protocol is so sparse and simple, writing servers or clients is 90 | quick and straightforward. 91 | 92 | 1. Introduction 93 | 94 | The Internet Gopher protocol is designed primarily to act as a 95 | distributed document delivery system. While documents (and services) 96 | reside on many servers, Gopher client software presents users with a 97 | hierarchy of items and directories much like a file system. In fact, 98 | the Gopher interface is designed to resemble a file system since a 99 | file system is a good model for locating documents and services. Why 100 | model a campus-wide information system after a file system? Several 101 | reasons: 102 | 103 | (a) A hierarchical arrangement of information is familiar to many 104 | users. Hierarchical directories containing items (such as 105 | documents, servers, and subdirectories) are widely used in 106 | electronic bulletin boards and other campus-wide information 107 | systems. People who access a campus-wide information server will 108 | expect some sort of hierarchical organization to the information 109 | presented. 110 | 111 | 112 | 113 | 114 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 2] 115 | 116 | RFC 1436 Gopher March 1993 117 | 118 | 119 | (b) A file-system style hierarchy can be expressed in a simple 120 | syntax. The syntax used for the internet Gopher protocol is 121 | easily understandable, and was designed to make debugging servers 122 | and clients easy. You can use Telnet to simulate an internet 123 | Gopher client's requests and observe the responses from a server. 124 | Special purpose software tools are not required. By keeping the 125 | syntax of the pseudo-file system client/server protocol simple, we 126 | can also achieve better performance for a very common user 127 | activity: browsing through the directory hierarchy. 128 | 129 | (c) Since Gopher originated in a University setting, one of the 130 | goals was for departments to have the option of publishing 131 | information from their inexpensive desktop machines, and since 132 | much of the information can be presented as simple text files 133 | arranged in directories, a protocol modeled after a file system 134 | has immediate utility. Because there can be a direct mapping from 135 | the file system on the user's desktop machine to the directory 136 | structure published via the Gopher protocol, the problem of 137 | writing server software for slow desktop systems is minimized. 138 | 139 | (d) A file system metaphor is extensible. By giving a "type" 140 | attribute to items in the pseudo-file system, it is possible to 141 | accommodate documents other than simple text documents. Complex 142 | database services can be handled as a separate type of item. A 143 | file-system metaphor does not rule out search or database-style 144 | queries for access to documents. A search-server type is also 145 | defined in this pseudo-file system. Such servers return "virtual 146 | directories" or list of documents matching user specified 147 | criteria. 148 | 149 | 2. The internet Gopher Model 150 | 151 | A detailed BNF rendering of the internet Gopher syntax is available 152 | in the appendix...but a close reading of the appendix may not be 153 | necessary to understand the internet Gopher protocol. 154 | 155 | In essence, the Gopher protocol consists of a client connecting to a 156 | server and sending the server a selector (a line of text, which may 157 | be empty) via a TCP connection. The server responds with a block of 158 | text terminated with a period on a line by itself, and closes the 159 | connection. No state is retained by the server between transactions 160 | with a client. The simple nature of the protocol stems from the need 161 | to implement servers and clients for the slow, smaller desktop 162 | computers (1 MB Macs and DOS machines), quickly, and efficiently. 163 | 164 | Below is a simple example of a client/server interaction; more 165 | complex interactions are dealt with later. Assume that a "well- 166 | known" Gopher server (this may be duplicated, details are discussed 167 | 168 | 169 | 170 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 3] 171 | 172 | RFC 1436 Gopher March 1993 173 | 174 | 175 | later) listens at a well known port for the campus (much like a 176 | domain-name server). The only configuration information the client 177 | software retains is this server's name and port number (in this 178 | example that machine is rawBits.micro.umn.edu and the port 70). In 179 | the example below the F character denotes the TAB character. 180 | 181 | Client: {Opens connection to rawBits.micro.umn.edu at port 70} 182 | 183 | Server: {Accepts connection but says nothing} 184 | 185 | Client: {Sends an empty line: Meaning "list what you have"} 186 | 187 | Server: {Sends a series of lines, each ending with CR LF} 188 | 0About internet GopherFStuff:About usFrawBits.micro.umn.eduF70 189 | 1Around University of MinnesotaFZ,5692,AUMFunderdog.micro.umn.eduF70 190 | 1Microcomputer News & PricesFPrices/Fpserver.bookstore.umn.eduF70 191 | 1Courses, Schedules, CalendarsFFevents.ais.umn.eduF9120 192 | 1Student-Staff DirectoriesFFuinfo.ais.umn.eduF70 193 | 1Departmental PublicationsFStuff:DP:FrawBits.micro.umn.eduF70 194 | {.....etc.....} 195 | . {Period on a line by itself} 196 | {Server closes connection} 197 | 198 | 199 | The first character on each line tells whether the line describes a 200 | document, directory, or search service (characters '0', '1', '7'; 201 | there are a handful more of these characters described later). The 202 | succeeding characters up to the tab form a user display string to be 203 | shown to the user for use in selecting this document (or directory) 204 | for retrieval. The first character of the line is really defining 205 | the type of item described on this line. In nearly every case, the 206 | Gopher client software will give the users some sort of idea about 207 | what type of item this is (by displaying an icon, a short text tag, 208 | or the like). 209 | 210 | The characters following the tab, up to the next tab form a selector 211 | string that the client software must send to the server to retrieve 212 | the document (or directory listing). The selector string should mean 213 | nothing to the client software; it should never be modified by the 214 | client. In practice, the selector string is often a pathname or 215 | other file selector used by the server to locate the item desired. 216 | The next two tab delimited fields denote the domain-name of the host 217 | that has this document (or directory), and the port at which to 218 | connect. If there are yet other tab delimited fields, the basic 219 | Gopher client should ignore them. A CR LF denotes the end of the 220 | item. 221 | 222 | 223 | 224 | 225 | 226 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 4] 227 | 228 | RFC 1436 Gopher March 1993 229 | 230 | 231 | In the example, line 1 describes a document the user will see as 232 | "About internet Gopher". To retrieve this document, the client 233 | software must send the retrieval string: "Stuff:About us" to 234 | rawBits.micro.umn.edu at port 70. If the client does this, the 235 | server will respond with the contents of the document, terminated by 236 | a period on a line by itself. A client might present the user with a 237 | view of the world something like the following list of items: 238 | 239 | 240 | About Internet Gopher 241 | Around the University of Minnesota... 242 | Microcomputer News & Prices... 243 | Courses, Schedules, Calendars... 244 | Student-Staff Directories... 245 | Departmental Publications... 246 | 247 | 248 | 249 | In this case, directories are displayed with an ellipsis and files 250 | are displayed without any. However, depending on the platform the 251 | client is written for and the author's taste, item types could be 252 | denoted by other text tags or by icons. For example, the UNIX 253 | curses-based client displays directories with a slash (/) following 254 | the name; Macintosh clients display directories alongside an icon of 255 | a folder. 256 | 257 | The user does not know or care that the items up for selection may 258 | reside on many different machines anywhere on the Internet. 259 | 260 | Suppose the user selects the line "Microcomputer News & Prices...". 261 | This appears to be a directory, and so the user expects to see 262 | contents of the directory upon request that it be fetched. The 263 | following lines illustrate the ensuing client-server interaction: 264 | 265 | 266 | Client: (Connects to pserver.bookstore.umn.edu at port 70) 267 | Server: (Accepts connection but says nothing) 268 | Client: Prices/ (Sends the magic string terminated by CRLF) 269 | Server: (Sends a series of lines, each ending with CR LF) 270 | 0About PricesFPrices/AboutusFpserver.bookstore.umn.eduF70 271 | 0Macintosh PricesFPrices/MacFpserver.bookstore.umn.eduF70 272 | 0IBM PricesFPrices/IckFpserver.bookstore.umn.eduF70 273 | 0Printer & Peripheral PricesFPrices/PPPFpserver.bookstore.umn.eduF70 274 | (.....etc.....) 275 | . (Period on a line by itself) 276 | (Server closes connection) 277 | 278 | 279 | 280 | 281 | 282 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 5] 283 | 284 | RFC 1436 Gopher March 1993 285 | 286 | 287 | 3. More details 288 | 289 | 3.1 Locating services 290 | 291 | Documents (or other services that may be viewed ultimately as 292 | documents, such as a student-staff phonebook) are linked to the 293 | machine they are on by the trio of selector string, machine domain- 294 | name, and IP port. It is assumed that there will be one well-known 295 | top-level or root server for an institution or campus. The 296 | information on this server may be duplicated by one or more other 297 | servers to avoid a single point of failure and to spread the load 298 | over several servers. Departments that wish to put up their own 299 | departmental servers need to register the machine name and port with 300 | the administrators of the top-level Gopher server, much the same way 301 | as they register a machine name with the campus domain-name server. 302 | An entry which points to the departmental server will then be made at 303 | the top level server. This ensures that users will be able to 304 | navigate their way down what amounts to a virtual hierarchical file 305 | system with a well known root to any campus server if they desire. 306 | 307 | Note that there is no requirement that a department register 308 | secondary servers with the central top-level server; they may just 309 | place a link to the secondary servers in their own primary servers. 310 | They may indeed place links to any servers they desire in their own 311 | server, thus creating a customized view of thethe Gopher information 312 | universe; links can of course point back at the top-level server. 313 | The virtual (networked) file system is therefore an arbitrary graph 314 | structure and not necessarily a rooted tree. The top-level node is 315 | merely one convenient, well-known point of entry. A set of Gopher 316 | servers linked in this manner may function as a campus-wide 317 | information system. 318 | 319 | Servers may of course point links at other than secondary servers. 320 | Indeed servers may point at other servers offering useful services 321 | anywhere on the internet. Viewed in this manner, Gopher can be seen 322 | as an Internet-wide information system. 323 | 324 | 3.2 Server portability and naming 325 | 326 | It is recommended that all registered servers have alias names 327 | (domain name system CNAME) that are used by Gopher clients to locate 328 | them. Links to these servers should use these alias names rather 329 | than the primary names. If information needs to be moved from one 330 | machine to another, a simple change of domain name system alias 331 | (CNAME) allows this to occur without any reconfiguration of clients 332 | in the field. In short, the domain name system may be used to re-map 333 | a server to a new address. There is nothing to prevent secondary 334 | servers or services from running on otherwise named servers or ports 335 | 336 | 337 | 338 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 6] 339 | 340 | RFC 1436 Gopher March 1993 341 | 342 | 343 | other than 70, however these should be reachable via a primary 344 | server. 345 | 346 | 3.3 Contacting server administrators 347 | 348 | It is recommended that every server administrator have a document 349 | called something like: "About Bogus University's Gopher server" as 350 | the first item in their server's top level directory. In this 351 | document should be a short description of what the server holds, as 352 | well as name, address, phone, and an e-mail address of the person who 353 | administers the server. This provides a way for users to get word to 354 | the administrator of a server that has inaccurate information or is 355 | not running correctly. It is also recommended that administrators 356 | place the date of last update in files for which such information 357 | matters to the users. 358 | 359 | 3.4 Modular addition of services 360 | 361 | The first character of each line in a server-supplied directory 362 | listing indicates whether the item is a file (character '0'), a 363 | directory (character '1'), or a search (character '7'). This is the 364 | base set of item types in the Gopher protocol. It is desirable for 365 | clients to be able to use different services and speak different 366 | protocols (simple ones such as finger; others such as CSO phonebook 367 | service, or Telnet, or X.500 directory service) as needs dictate. 368 | CSO phonebook service is a client/server phonebook system typically 369 | used at Universities to publish names, e-mail addresses, and so on. 370 | The CSO phonebook software was developed at the University of 371 | Illinois and is also sometimes refered to as ph or qi. For example, 372 | if a server-supplied directory listing marks a certain item with type 373 | character '2', then it means that to use this item, the client must 374 | speak the CSO protocol. This removes the need to be able to 375 | anticipate all future needs and hard-wire them in the basic Internet 376 | Gopher protocol; it keeps the basic protocol extremely simple. In 377 | spite of this simplicity, the scheme has the capability to expand and 378 | change with the times by adding an agreed upon type-character for a 379 | new service. This also allows the client implementations to evolve 380 | in a modular fashion, simply by dropping in a module (or launching a 381 | new process) for some new service. The servers for the new service 382 | of course have to know nothing about Internet Gopher; they can just 383 | be off-the shelf CSO, X.500, or other servers. We do not however, 384 | encourage arbitrary or machine-specific proliferation of service 385 | types in the basic Gopher protocol. 386 | 387 | On the other hand, subsets of other document retrieval schemes may be 388 | mapped onto the Gopher protocol by means of "gateway-servers". 389 | Examples of such servers include Gopher-to-FTP gateways, Gopher-to- 390 | archie gateways, Gopher-to-WAIS gateways, etc. There are a number of 391 | 392 | 393 | 394 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 7] 395 | 396 | RFC 1436 Gopher March 1993 397 | 398 | 399 | advantages of such mechanisms. First, a relatively powerful server 400 | machine inherits both the intelligence and work, rather than the more 401 | modest, inexpensive desktop system that typically runs client 402 | software or basic server software. Equally important, clients do not 403 | have to be modified to take advantage of a new resource. 404 | 405 | 3.5 Building clients 406 | 407 | A client simply sends the retrieval string to a server if it wants to 408 | retrieve a document or view the contents of a directory. Of course, 409 | each host may have pointers to other hosts, resulting in a "graph" 410 | (not necessarily a rooted tree) of hosts. The client software may 411 | save (or rather "stack") the locations that it has visited in search 412 | of a document. The user could therefore back out of the current 413 | location by unwinding the stack. Alternatively, a client with 414 | multiple-window capability might just be able to display more than 415 | one directory or document at the same time. 416 | 417 | A smart client could cache the contents of visited directories 418 | (rather than just the directory's item descriptor), thus avoiding 419 | network transactions if the information has been previously 420 | retrieved. 421 | 422 | If a client does not understand what a say, type 'B' item (not a core 423 | item) is, then it may simply ignore the item in the directory 424 | listing; the user never even has to see it. Alternatively, the item 425 | could be displayed as an unknown type. 426 | 427 | Top-level or primary servers for a campus are likely to get more 428 | traffic than secondary servers, and it would be less tolerable for 429 | such primary servers to be down for any long time. So it makes sense 430 | to "clone" such important servers and construct clients that can 431 | randomly choose between two such equivalent primary servers when they 432 | first connect (to balance server load), moving to one if the other 433 | seems to be down. In fact, smart client implementations do this 434 | clone server and load balancing. Alternatively, it may make sense to 435 | have the domain name system return one of a set of redundant of 436 | server's IP address to load balance betwen redundant sets of 437 | important servers. 438 | 439 | 3.6 Building ordinary internet Gopher servers 440 | 441 | The retrieval string sent to the server might be a path to a file or 442 | directory. It might be the name of a script, an application or even 443 | a query that generates the document or directory returned. The basic 444 | server uses the string it gets up to but not including a CR-LF or a 445 | TAB, whichever comes first. 446 | 447 | 448 | 449 | 450 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 8] 451 | 452 | RFC 1436 Gopher March 1993 453 | 454 | 455 | All intelligence is carried by the server implementation rather than 456 | the protocol. What you build into more exotic servers is up to you. 457 | Server implementations may grow as needs dictate and time allows. 458 | 459 | 3.7 Special purpose servers 460 | 461 | There are two special server types (beyond the normal Gopher server) 462 | also discussed below: 463 | 464 | 1. A server directory listing can point at a CSO nameserver (the 465 | server returns a type character of '2') to allow a campus 466 | student-staff phonebook lookup service. This may show up on the 467 | user's list of choices, perhaps preceded by the icon of a phone- 468 | book. If this item is selected, the client software must resort 469 | to a pure CSO nameserver protocol when it connects to the 470 | appropriate host. 471 | 472 | 2. A server can also point at a "search server" (returns a first 473 | character of '7'). Such servers may implement campus network (or 474 | subnet) wide searching capability. The most common search servers 475 | maintain full-text indexes on the contents of text documents held 476 | by some subset of Gopher servers. Such a "full-text search 477 | server" responds to client requests with a list of all documents 478 | that contain one or more words (the search criteria). The client 479 | sends the server the selector string, a tab, and the search string 480 | (words to search for). If the selector string is empty, the client 481 | merely sends the search string. The server returns the equivalent 482 | of a directory listing for documents matching the search criteria. 483 | Spaces between words are usually implied Boolean ANDs (although in 484 | different implementations or search types, this may not 485 | necessarily be true). 486 | 487 | The CSO addition exists for historical reasons: at time of design, 488 | the campus phone-book servers at the University of Minnesota used the 489 | CSO protocol and it seemed simplest to just engulf them. The index- 490 | server is however very much a Gopher in spirit, albeit with a slight 491 | twist in the meaning of the selector-string. Index servers are a 492 | natural place to incorperate gateways to WAIS and WHOIS services. 493 | 494 | 3.7.1 Building CSO-servers 495 | 496 | A CSO Nameserver implementation for UNIX and associated documentation 497 | is available by anonymous ftp from uxa.cso.uiuc.edu. We do not 498 | anticipate implementing it on other machines. 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 9] 507 | 508 | RFC 1436 Gopher March 1993 509 | 510 | 511 | 3.7.2 Building full-text search servers 512 | 513 | A full-text search server is a special-purpose server that knows 514 | about the Gopher scheme for retrieving documents. These servers 515 | maintain a full-text index of the contents of plain text documents on 516 | Gopher servers in some specified domain. A Gopher full-text search 517 | server was implemented using several NeXTstations because it was easy 518 | to take advantage of the full-text index/search engine built into the 519 | NeXT system software. A search server for generic UNIX systems based 520 | on the public domain WAIS search engine, is also available and 521 | currently an optional part of the UNIX gopher server. In addition, 522 | at least one implementation of the gopher server incorperates a 523 | gateway to WAIS servers by presenting the WAIS servers to gopherspace 524 | as full-text search servers. The gopher<->WAIS gateway servers does 525 | the work of translating from gopher protocol to WAIS so unmodified 526 | gopher clients can access WAIS servers via the gateway server. 527 | 528 | By using several index servers (rather than a monolithic index 529 | server) indexes may be searched in parallel (although the client 530 | software is not aware of this). While maintaining full-text indexes 531 | of documents distributed over many machines may seem a daunting task, 532 | the task can be broken into smaller pieces (update only a portion of 533 | the indexes, search several partial indexes in parallel) so that it 534 | is manageable. By spreading this task over several small, cheap (and 535 | fast) workstations it is possible to take advantage of fine-grain 536 | parallelism. Again, the client software is not aware of this. Client 537 | software only needs to know that it can send a search string to an 538 | index server and will receive a list of documents that contain the 539 | words in the search string. 540 | 541 | 3.8 Item type characters 542 | 543 | The client software decides what items are available by looking at 544 | the first character of each line in a directory listing. Augmenting 545 | this list can extend the protocol. A list of defined item-type 546 | characters follows: 547 | 548 | 0 Item is a file 549 | 1 Item is a directory 550 | 2 Item is a CSO phone-book server 551 | 3 Error 552 | 4 Item is a BinHexed Macintosh file. 553 | 5 Item is DOS binary archive of some sort. 554 | Client must read until the TCP connection closes. Beware. 555 | 6 Item is a UNIX uuencoded file. 556 | 7 Item is an Index-Search server. 557 | 8 Item points to a text-based telnet session. 558 | 9 Item is a binary file! 559 | 560 | 561 | 562 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 10] 563 | 564 | RFC 1436 Gopher March 1993 565 | 566 | 567 | Client must read until the TCP connection closes. Beware. 568 | + Item is a redundant server 569 | T Item points to a text-based tn3270 session. 570 | g Item is a GIF format graphics file. 571 | I Item is some kind of image file. Client decides how to display. 572 | 573 | Characters '0' through 'Z' are reserved. Local experiments should 574 | use other characters. Machine-specific extensions are not 575 | encouraged. Note that for type 5 or type 9 the client must be 576 | prepared to read until the connection closes. There will be no 577 | period at the end of the file; the contents of these files are binary 578 | and the client must decide what to do with them based perhaps on the 579 | .xxx extension. 580 | 581 | 3.9 User display strings and server selector strings 582 | 583 | User display strings are intended to be displayed on a line on a 584 | typical screen for a user's viewing pleasure. While many screens can 585 | accommodate 80 character lines, some space is needed to display a tag 586 | of some sort to tell the user what sort of item this is. Because of 587 | this, the user display string should be kept under 70 characters in 588 | length. Clients may truncate to a length convenient to them. 589 | 590 | 4. Simplicity is intentional 591 | 592 | As far as possible we desire any new features to be carried as new 593 | protocols that will be hidden behind new document-types. The 594 | internet Gopher philosophy is: 595 | 596 | (a) Intelligence is held by the server. Clients have the option 597 | of being able to access new document types (different, other types 598 | of servers) by simply recognizing the document-type character. 599 | Further intelligence to be borne by the protocol should be 600 | minimized. 601 | 602 | (b) The well-tempered server ought to send "text" (unless a file 603 | must be transferred as raw binary). Should this text include 604 | tabs, formfeeds, frufru? Probably not, but rude servers will 605 | probably send them anyway. Publishers of documents should be 606 | given simple tools (filters) that will alert them if there are any 607 | funny characters in the documents they wish to publish, and give 608 | them the opportunity to strip the questionable characters out; the 609 | publisher may well refuse. 610 | 611 | (c) The well-tempered client should do something reasonable with 612 | funny characters received in text; filter them out, leave them in, 613 | whatever. 614 | 615 | 616 | 617 | 618 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 11] 619 | 620 | RFC 1436 Gopher March 1993 621 | 622 | 623 | Appendix 624 | 625 | Paul's NQBNF (Not Quite BNF) for the Gopher Protocol. 626 | 627 | Note: This is modified BNF (as used by the Pascal people) with a few 628 | English modifiers thrown in. Stuff enclosed in '{}' can be 629 | repeated zero or more times. Stuff in '[]' denotes a set of 630 | items. The '-' operator denotes set subtraction. 631 | 632 | 633 | Directory Entity 634 | 635 | CR-LF ::= ASCII Carriage Return Character followed by Line Feed 636 | character. 637 | 638 | Tab ::= ASCII Tab character. 639 | 640 | NUL ::= ASCII NUL character. 641 | 642 | UNASCII ::= ASCII - [Tab CR-LF NUL]. 643 | 644 | Lastline ::= '.'CR-LF. 645 | 646 | TextBlock ::= Block of ASCII text not containing Lastline pattern. 647 | 648 | Type ::= UNASCII. 649 | 650 | RedType ::= '+'. 651 | 652 | User_Name ::= {UNASCII}. 653 | 654 | Selector ::= {UNASCII}. 655 | 656 | Host ::= {{UNASCII - ['.']} '.'} {UNASCII - ['.']}. 657 | 658 | Note: This is a Fully Qualified Domain Name as defined in RFC 1034. 659 | (e.g., gopher.micro.umn.edu) Hosts that have a CR-LF 660 | TAB or NUL in their name get what they deserve. 661 | 662 | Digit ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' . 663 | 664 | DigitSeq ::= digit {digit}. 665 | 666 | Port ::= DigitSeq. 667 | 668 | Note: Port corresponds the the TCP Port Number, its value should 669 | be in the range [0..65535]; port 70 is officially assigned 670 | to gopher. 671 | 672 | 673 | 674 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 12] 675 | 676 | RFC 1436 Gopher March 1993 677 | 678 | 679 | DirEntity ::= Type User_Name Tab Selector Tab Host Tab Port CR-LF 680 | {RedType User_Name Tab Selector Tab Host Tab Port CR-LF} 681 | 682 | 683 | 684 | Notes: 685 | 686 | It is *highly* recommended that the User_Name field contain only 687 | printable characters, since many different clients will be using 688 | it. However if eight bit characters are used, the characters 689 | should conform with the ISO Latin1 Character Set. The length of 690 | the User displayable line should be less than 70 Characters; longer 691 | lines may not fit across some screens. 692 | 693 | The Selector string should be no longer than 255 characters. 694 | 695 | 696 | Menu Entity 697 | 698 | Menu ::= {DirEntity} Lastline. 699 | 700 | 701 | Menu Transaction (Type 1 item) 702 | 703 | C: Opens Connection 704 | S: Accepts Connection 705 | C: Sends Selector String 706 | S: Sends Menu Entity 707 | 708 | Connection is closed by either client or server (typically server). 709 | 710 | 711 | Textfile Entity 712 | 713 | TextFile ::= {TextBlock} Lastline 714 | 715 | Note: Lines beginning with periods must be prepended with an extra 716 | period to ensure that the transmission is not terminated early. 717 | The client should strip extra periods at the beginning of the line. 718 | 719 | 720 | TextFile Transaction (Type 0 item) 721 | 722 | C: Opens Connection. 723 | S: Accepts connection 724 | C: Sends Selector String. 725 | S: Sends TextFile Entity. 726 | 727 | 728 | 729 | 730 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 13] 731 | 732 | RFC 1436 Gopher March 1993 733 | 734 | 735 | Connection is closed by either client or server (typically server). 736 | 737 | Note: The client should be prepared for the server closing the 738 | connection without sending the Lastline. This allows the 739 | client to use fingerd servers. 740 | 741 | 742 | Full-Text Search Transaction (Type 7 item) 743 | 744 | Word ::= {UNASCII - ' '} 745 | BoolOp ::= 'and' | 'or' | 'not' | SPACE 746 | SearchStr ::= Word {{SPACE BoolOp} SPACE Word} 747 | 748 | C: Opens Connection. 749 | C: Sends Selector String, Tab, Search String. 750 | S: Sends Menu Entity. 751 | 752 | Note: In absence of 'and', 'or', or 'not' operators, a SPACE is 753 | regarded as an implied 'and' operator. Expression is evaluated 754 | left to right. Further, not all search engines or search 755 | gateways currently implemented have the boolean operators 756 | implemented. 757 | 758 | Binary file Transaction (Type 9 or 5 item) 759 | 760 | C: Opens Connection. 761 | S: Accepts connection 762 | C: Sends Selector String. 763 | S: Sends a binary file and closes connection when done. 764 | 765 | 766 | Syntactic Meaning for Directory Entities 767 | 768 | 769 | The client should interpret the type field as follows: 770 | 771 | 0 The item is a TextFile Entity. 772 | Client should use a TextFile Transaction. 773 | 774 | 1 The item is a Menu Entity. 775 | Client should use a Menu Transaction. 776 | 777 | 2 The information applies to a CSO phone book entity. 778 | Client should talk CSO protocol. 779 | 780 | 3 Signals an error condition. 781 | 782 | 4 Item is a Macintosh file encoded in BINHEX format 783 | 784 | 785 | 786 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 14] 787 | 788 | RFC 1436 Gopher March 1993 789 | 790 | 791 | 5 Item is PC-DOS binary file of some sort. Client gets to decide. 792 | 793 | 6 Item is a uuencoded file. 794 | 795 | 7 The information applies to a Index Server. 796 | Client should use a FullText Search transaction. 797 | 798 | 8 The information applies to a Telnet session. 799 | Connect to given host at given port. The name to login as at this 800 | host is in the selector string. 801 | 802 | 9 Item is a binary file. Client must decide what to do with it. 803 | 804 | + The information applies to a duplicated server. The information 805 | contained within is a duplicate of the primary server. The primary 806 | server is defined as the last DirEntity that is has a non-plus 807 | "Type" field. The client should use the transaction as defined by 808 | the primary server Type field. 809 | 810 | g Item is a GIF graphic file. 811 | 812 | I Item is some kind of image file. Client gets to decide. 813 | 814 | T The information applies to a tn3270 based telnet session. 815 | Connect to given host at given port. The name to login as at this 816 | host is in the selector string. 817 | 818 | Security Considerations 819 | 820 | Security issues are not discussed in this memo. 821 | 822 | Authors' Addresses 823 | 824 | Farhad Anklesaria 825 | Computer and Information Services, University of Minnesota 826 | Room 152 Shepherd Labs 827 | 100 Union Street SE 828 | Minneapolis, MN 55455 829 | 830 | Phone: (612) 625 1300 831 | EMail: fxa@boombox.micro.umn.edu 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 15] 843 | 844 | RFC 1436 Gopher March 1993 845 | 846 | 847 | Mark McCahill 848 | Computer and Information Services, University of Minnesota 849 | Room 152 Shepherd Labs 850 | 100 Union Street SE 851 | Minneapolis, MN 55455 852 | 853 | Phone: (612) 625 1300 854 | EMail: mpm@boombox.micro.umn.edu 855 | 856 | 857 | Paul Lindner 858 | Computer and Information Services, University of Minnesota 859 | Room 152 Shepherd Labs 860 | 100 Union Street SE 861 | Minneapolis, MN 55455 862 | 863 | Phone: (612) 625 1300 864 | EMail: lindner@boombox.micro.umn.edu 865 | 866 | 867 | David Johnson 868 | Computer and Information Services, University of Minnesota 869 | Room 152 Shepherd Labs 870 | 100 Union Street SE 871 | Minneapolis, MN 55455 872 | 873 | Phone: (612) 625 1300 874 | EMail: dmj@boombox.micro.umn.edu 875 | 876 | 877 | Daniel Torrey 878 | Computer and Information Services, University of Minnesota 879 | Room 152 Shepherd Labs 880 | 100 Union Street SE 881 | Minneapolis, MN 55455 882 | 883 | Phone: (612) 625 1300 884 | EMail: daniel@boombox.micro.umn.edu 885 | 886 | 887 | Bob Alberti 888 | Computer and Information Services, University of Minnesota 889 | Room 152 Shepherd Labs 890 | 100 Union Street SE 891 | Minneapolis, MN 55455 892 | 893 | Phone: (612) 625 1300 894 | EMail: alberti@boombox.micro.umn.edu 895 | 896 | 897 | 898 | Anklesari, McCahill, Lindner, Johnson, Torrey & Alberti [Page 16] 899 | 900 | 901 | 902 | . 903 | -------------------------------------------------------------------------------- /docs/web.nix: -------------------------------------------------------------------------------- 1 | { depotSrc ? builtins.fetchGit { 2 | url = "https://code.tvl.fyi"; 3 | ref = "canon"; 4 | rev = "1c7083dafc6e6572803179e51bfb8b1c45e7ca6b"; 5 | } 6 | }: 7 | 8 | let 9 | depot = import depotSrc { }; 10 | in 11 | 12 | depot.users.sterni.htmlman { 13 | title = "spacecookie"; 14 | description = '' 15 | * [Source (GitHub)](https://github.com/sternenseemann/spacecookie) 16 | * [Source (Mirror)](https://code.sterni.lv/spacecookie) 17 | 18 | spacecookie is a gopher server daemon and library written in Haskell. 19 | 20 | Below you can find the user's documentation in the form of a few man pages. 21 | A more general overview of the software and installation instructions can be 22 | found in the 23 | [README](https://github.com/sternenseemann/spacecookie/blob/master/README.md). 24 | 25 | The developer's documentation for the bundled library is 26 | [located on Hackage](https://hackage.haskell.org/package/spacecookie). 27 | ''; 28 | manDir = ./man; 29 | pages = [ 30 | { name = "spacecookie"; section = 1; } 31 | { name = "spacecookie.json"; section = 5; } 32 | { name = "spacecookie.gophermap"; section = 5; } 33 | ]; 34 | linkXr = "inManDir"; 35 | } 36 | -------------------------------------------------------------------------------- /etc/spacecookie.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostname" : "localhost", 3 | "listen" : { 4 | "addr" : "::", 5 | "port" : 70 6 | }, 7 | "user" : null, 8 | "root" : "/srv/gopher", 9 | "log" : { 10 | "enable" : true, 11 | "hide-ips" : true, 12 | "hide-time" : false, 13 | "level" : "info" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /etc/spacecookie.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Spacecookie Gopher Daemon 3 | Requires=spacecookie.socket 4 | 5 | [Service] 6 | Type=notify 7 | ExecStart=/path/to/spacecookie /path/to/spacecookie.json 8 | FileDescriptorStoreMax=1 9 | DynamicUser=true 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /etc/spacecookie.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Socket for the Spacecookie Gopher Daemon 3 | 4 | [Socket] 5 | BindIPv6Only=both 6 | ListenStream=[::]:70 7 | -------------------------------------------------------------------------------- /server/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | import Network.Spacecookie.Config 3 | import Network.Spacecookie.FileType 4 | import Network.Spacecookie.Path 5 | import Network.Spacecookie.Systemd 6 | 7 | import Paths_spacecookie (version) 8 | 9 | import Network.Gopher 10 | import Network.Gopher.Util.Gophermap 11 | import qualified Data.ByteString as B 12 | import Control.Exception (catches, Handler (..)) 13 | import Control.Monad (when, unless) 14 | import Data.Aeson (eitherDecodeFileStrict') 15 | import Data.Attoparsec.ByteString (parseOnly) 16 | import Data.Bifunctor (first) 17 | import Data.ByteString.Builder (Builder ()) 18 | import Data.Either (rights) 19 | import Data.Maybe (fromMaybe) 20 | import Data.Version (showVersion) 21 | import System.Console.GetOpt 22 | import System.Directory (doesFileExist, listDirectory) 23 | import System.Environment 24 | import System.Exit 25 | import System.FilePath.Posix.ByteString ( RawFilePath, takeFileName, () 26 | , dropDrive, decodeFilePath 27 | , encodeFilePath, normalise) 28 | import qualified System.Log.FastLogger as FL 29 | import System.Posix.Directory (changeWorkingDirectory) 30 | import System.Socket (SocketException ()) 31 | import System.Posix.User 32 | 33 | data Flags = Version | Usage 34 | 35 | options :: [OptDescr Flags] 36 | options = 37 | [ Option "h" [ "help", "usage" ] (NoArg Usage) "Print usage information" 38 | , Option [] [ "version" ] (NoArg Version) "Show used version of spacecookie" 39 | ] 40 | 41 | main :: IO () 42 | main = do 43 | args <- getArgs 44 | case getOpt Permute options args of 45 | ([], [configFile], []) -> runServer configFile 46 | -- this works because we only have two flags atm 47 | ([Version], _, []) -> putStrLn $ showVersion version 48 | (_, _, []) -> printUsage 49 | (_, _, es) -> die . mconcat $ 50 | "errors occurred while parsing options:\n":es 51 | 52 | runServer :: FilePath -> IO () 53 | runServer configFile = do 54 | doesFileExist configFile >>= 55 | (flip unless) (die "could not open config file") 56 | config' <- eitherDecodeFileStrict' configFile 57 | case config' of 58 | Left err -> die $ "failed to parse config: " ++ err 59 | Right config -> do 60 | changeWorkingDirectory (rootDirectory config) 61 | (logHandler, logStopAction) <- fromMaybe (Nothing, pure ()) 62 | . fmap (first Just) <$> makeLogHandler (logConfig config) 63 | let cfg = GopherConfig 64 | { cServerName = serverName config 65 | , cListenAddr = listenAddr config 66 | , cServerPort = serverPort config 67 | , cLogHandler = logHandler 68 | } 69 | logIO = fromMaybe noLog logHandler 70 | 71 | let setupFailureHandler e = do 72 | logIO GopherLogLevelError 73 | $ "Exception occurred in setup step: " 74 | <> toGopherLogStr (show e) 75 | logStopAction 76 | exitFailure 77 | catchSetupFailure a = a `catches` 78 | [ Handler (setupFailureHandler :: SystemdException -> IO ()) 79 | , Handler (setupFailureHandler :: SocketException -> IO ()) 80 | ] 81 | 82 | catchSetupFailure $ runGopherManual 83 | (systemdSocket cfg) 84 | (afterSocketSetup logIO config) 85 | (\s -> do 86 | _ <- notifyStopping 87 | logStopAction 88 | systemdStoreOrClose s) 89 | cfg 90 | (spacecookie logIO) 91 | 92 | -- | If 'runUserName' is configured, call 'setGroupID' and 'setUserID' 93 | -- to switch to the given user and their primary group. 94 | -- Requires special privileges (usually root). Will raise an exception if 95 | -- either the user does not exist or the current user has no permission to 96 | -- change UID/GID. 97 | -- 98 | -- After that, notify systemd that we are ready if applicable. 99 | afterSocketSetup :: GopherLogHandler -> Config -> IO () 100 | afterSocketSetup logIO cfg = do 101 | case runUserName cfg of 102 | Nothing -> pure () 103 | Just username -> do 104 | user <- getUserEntryForName username 105 | setGroupID $ userGroupID user 106 | setUserID $ userID user 107 | logIO GopherLogLevelInfo $ "Changed to user " <> toGopherLogStr username 108 | _ <- notifyReady 109 | pure () 110 | 111 | printUsage :: IO () 112 | printUsage = do 113 | n <- getProgName 114 | putStrLn . flip usageInfo options $ 115 | mconcat [ "Usage: ", n, " CONFIG\n" ] 116 | 117 | makeLogHandler :: LogConfig -> IO (Maybe (GopherLogHandler, IO ())) 118 | makeLogHandler lc 119 | | not (logEnable lc) = pure Nothing 120 | | otherwise = 121 | let wrapTimedLogger :: FL.TimedFastLogger -> FL.FastLogger 122 | wrapTimedLogger logger str = logger $ (\t -> 123 | "[" <> FL.toLogStr t <> "]" <> str) 124 | formatLevel lvl = 125 | case lvl of 126 | GopherLogLevelInfo -> "[info] " 127 | GopherLogLevelWarn -> "[warn] " 128 | GopherLogLevelError -> "[err ] " 129 | processMsg = 130 | if logHideIps lc 131 | then hideSensitive 132 | else id 133 | logHandler :: FL.FastLogger -> GopherLogLevel -> GopherLogStr -> IO () 134 | logHandler logger lvl msg = when (lvl <= logLevel lc) . logger 135 | $ formatLevel lvl 136 | <> ((FL.toLogStr :: Builder -> FL.LogStr) . fromGopherLogStr . processMsg $ msg) 137 | <> "\n" 138 | logType = FL.LogStderr FL.defaultBufSize 139 | in do 140 | (logger, cleanup) <- 141 | if logHideTime lc 142 | then FL.newFastLogger logType 143 | else first wrapTimedLogger <$> do 144 | timeCache <- FL.newTimeCache FL.simpleTimeFormat 145 | FL.newTimedFastLogger timeCache logType 146 | pure $ Just (logHandler logger, cleanup) 147 | 148 | noLog :: GopherLogHandler 149 | noLog = const . const $ pure () 150 | 151 | spacecookie :: GopherLogHandler -> GopherRequest -> IO GopherResponse 152 | spacecookie logger req = do 153 | let selector = requestSelector req 154 | path = normalise $ dropDrive (sanitizePath selector) 155 | pt <- gopherFileType path 156 | 157 | case pt of 158 | Left PathIsNotAllowed -> 159 | pure . ErrorResponse $ mconcat 160 | [ "Accessing '", selector, "' is not allowed." ] 161 | Left PathDoesNotExist -> pure $ 162 | if "URL:" `B.isPrefixOf` selector 163 | then ErrorResponse $ mconcat 164 | [ "spacecookie does not support proxying HTTP, " 165 | , "try using a gopher client that supports URL: selectors. " 166 | , "If you tried to request a resource called '" 167 | , selector, "', it does not exist." ] 168 | else ErrorResponse $ mconcat 169 | [ "The requested resource '", selector 170 | , "' does not exist or is not available." ] 171 | Right ft -> 172 | case ft of 173 | Error -> pure $ ErrorResponse $ "An unknown error occurred" 174 | -- always use gophermapResponse which falls back 175 | -- to directoryResponse if there is no gophermap file 176 | Directory -> gophermapResponse logger path 177 | _ -> fileResponse logger path 178 | 179 | fileResponse :: GopherLogHandler -> RawFilePath -> IO GopherResponse 180 | fileResponse _ path = FileResponse <$> B.readFile (decodeFilePath path) 181 | 182 | directoryResponse :: GopherLogHandler -> RawFilePath -> IO GopherResponse 183 | directoryResponse _ path = 184 | let makeItem :: Either a GopherFileType -> RawFilePath -> Either a GopherMenuItem 185 | makeItem t file = do 186 | fileType <- t 187 | pure $ 188 | Item fileType (takeFileName file) file Nothing Nothing 189 | in do 190 | dir <- map ((path ) . encodeFilePath) 191 | <$> listDirectory (decodeFilePath path) 192 | fileTypes <- mapM gopherFileType dir 193 | 194 | pure . MenuResponse . rights 195 | $ zipWith makeItem fileTypes (map makeAbsolute dir) 196 | 197 | gophermapResponse :: GopherLogHandler -> RawFilePath -> IO GopherResponse 198 | gophermapResponse logger path = do 199 | let gophermap = path ".gophermap" 200 | gophermapWide = decodeFilePath gophermap 201 | exists <- doesFileExist gophermapWide 202 | parsed <- 203 | if exists 204 | then parseOnly parseGophermap <$> B.readFile gophermapWide 205 | else pure $ Left "Gophermap file does not exist" 206 | case parsed of 207 | Left err -> do 208 | when exists . logger GopherLogLevelWarn 209 | $ "Could not parse gophermap at " <> toGopherLogStr gophermap 210 | <> ": " <> toGopherLogStr err 211 | directoryResponse logger path 212 | Right right -> pure 213 | $ gophermapToDirectoryResponse (makeAbsolute path) right 214 | -------------------------------------------------------------------------------- /server/Network/Spacecookie/Config.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE CPP #-} 3 | module Network.Spacecookie.Config 4 | ( Config (..) 5 | , LogConfig (..) 6 | ) where 7 | 8 | import Control.Monad (mzero, join) 9 | import Control.Applicative ((<|>)) 10 | import Data.Aeson 11 | import Data.Aeson.Types (Parser ()) 12 | import Data.ByteString (ByteString ()) 13 | import qualified Data.ByteString.UTF8 as UTF8 14 | import Data.Text (toLower, Text ()) 15 | import Network.Gopher (GopherLogLevel (..)) 16 | 17 | data Config 18 | = Config 19 | { serverName :: ByteString 20 | , listenAddr :: Maybe ByteString 21 | , serverPort :: Integer 22 | , runUserName :: Maybe String 23 | , rootDirectory :: FilePath 24 | , logConfig :: LogConfig 25 | } 26 | 27 | -- We only use string literals with 'maybePath', so we can just switch between 28 | -- Key and Text, since both have an IsString instance for OverloadedStrings. 29 | #if MIN_VERSION_aeson(2,0,0) 30 | maybePath :: FromJSON a => [Key] -> Object -> Parser (Maybe a) 31 | #else 32 | maybePath :: FromJSON a => [Text] -> Object -> Parser (Maybe a) 33 | #endif 34 | maybePath [] _ = fail "got empty path" 35 | maybePath [x] v = v .:? x 36 | maybePath (x:xs) v = v .:? x >>= fmap join . traverse (maybePath xs) 37 | 38 | instance FromJSON Config where 39 | parseJSON (Object v) = Config 40 | <$> v .: "hostname" 41 | <*> maybePath [ "listen", "addr" ] v 42 | <*> parseListenPort v .!= 70 43 | <*> v .:? "user" 44 | <*> v .: "root" 45 | <*> v .:? "log" .!= defaultLogConfig 46 | parseJSON _ = mzero 47 | 48 | -- Use '(<|>)' over the 'Maybe's in the parser rather 49 | -- to only fallback on 'Nothing' and not on @empty@. 50 | -- This way a parse error in listen → port doesn't get 51 | -- promoted to just 'Nothing'. 52 | parseListenPort :: Object -> Parser (Maybe Integer) 53 | parseListenPort v = (<|>) 54 | <$> maybePath [ "listen", "port" ] v 55 | <*> (v .:? "port") 56 | 57 | data LogConfig 58 | = LogConfig 59 | { logEnable :: Bool 60 | , logHideIps :: Bool 61 | , logHideTime :: Bool 62 | , logLevel :: GopherLogLevel 63 | } 64 | 65 | defaultLogConfig :: LogConfig 66 | defaultLogConfig = LogConfig True True False GopherLogLevelInfo 67 | 68 | instance FromJSON LogConfig where 69 | parseJSON (Object v) = LogConfig 70 | <$> v .:? "enable" .!= logEnable defaultLogConfig 71 | <*> v .:? "hide-ips" .!= logHideIps defaultLogConfig 72 | <*> v .:? "hide-time" .!= logHideTime defaultLogConfig 73 | <*> v .:? "level" .!= logLevel defaultLogConfig 74 | parseJSON _ = mzero 75 | 76 | -- auxiliary instances for types that have no default instance 77 | instance FromJSON GopherLogLevel where 78 | parseJSON (String s) = 79 | case toLower s of 80 | "info" -> pure GopherLogLevelInfo 81 | "warn" -> pure GopherLogLevelWarn 82 | "error" -> pure GopherLogLevelError 83 | _ -> mzero 84 | parseJSON _ = mzero 85 | 86 | instance FromJSON ByteString where 87 | parseJSON s@(String _) = UTF8.fromString <$> parseJSON s 88 | parseJSON _ = mzero 89 | -------------------------------------------------------------------------------- /server/Network/Spacecookie/FileType.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Network.Spacecookie.FileType 3 | ( PathError (..) 4 | , gopherFileType 5 | -- exposed for tests 6 | , lookupSuffix 7 | , checkNoDotFiles 8 | ) where 9 | 10 | import Network.Spacecookie.Path (containsDotFiles) 11 | 12 | import qualified Data.ByteString as B 13 | import Data.Char (ord, chr, toLower) 14 | import qualified Data.Map as M 15 | import Data.Maybe (fromMaybe) 16 | import Data.Word (Word8 ()) 17 | import Network.Gopher (GopherFileType (..)) 18 | import System.Directory (doesDirectoryExist, doesFileExist) 19 | import System.FilePath.Posix.ByteString ( RawFilePath, takeExtension 20 | , decodeFilePath) 21 | 22 | fileTypeMap :: M.Map RawFilePath GopherFileType 23 | fileTypeMap = M.fromList 24 | [ (".gif", GifFile) 25 | , (".png", ImageFile) 26 | , (".jpg", ImageFile) 27 | , (".jpeg", ImageFile) 28 | , (".tiff", ImageFile) 29 | , (".tif", ImageFile) 30 | , (".bmp", ImageFile) 31 | , (".webp", ImageFile) 32 | , (".apng", ImageFile) 33 | , (".mng", ImageFile) 34 | , (".heif", ImageFile) 35 | , (".heifs", ImageFile) 36 | , (".heic", ImageFile) 37 | , (".heics", ImageFile) 38 | , (".avci", ImageFile) 39 | , (".avcs", ImageFile) 40 | , (".avif", ImageFile) 41 | , (".avifs", ImageFile) 42 | , (".ico", ImageFile) 43 | , (".svg", ImageFile) 44 | , (".raw", ImageFile) -- TODO: RAW files should maybe be binary files? 45 | , (".cr2", ImageFile) 46 | , (".nef", ImageFile) 47 | , (".json", File) 48 | , (".txt", File) 49 | , (".text", File) 50 | , (".md", File) 51 | , (".mdown", File) 52 | , (".mkdn", File) 53 | , (".mkd", File) 54 | , (".markdown", File) 55 | , (".adoc", File) 56 | , (".rst", File) 57 | , (".zip", BinaryFile) 58 | , (".tar", BinaryFile) 59 | , (".gz", BinaryFile) 60 | , (".bzip2", BinaryFile) 61 | , (".xz", BinaryFile) 62 | , (".tgz", BinaryFile) 63 | , (".doc", BinaryFile) 64 | , (".hqx", BinHexMacintoshFile) 65 | ] 66 | 67 | -- | Transform a 'Word8' to lowercase if the solution is in bounds. 68 | -- 69 | -- >>> asciiToLower 65 70 | -- 97 71 | -- >>> asciiToLower 97 72 | -- 97 73 | -- >>> asciiToLower 220 74 | -- 220 75 | -- >>> asciiToLower 252 76 | -- 252 77 | asciiToLower :: Word8 -> Word8 78 | asciiToLower orig 79 | | orig > 127 || lower > 127 = orig 80 | | otherwise = fromIntegral lower 81 | where lower :: Int 82 | lower = ord . toLower . chr . fromIntegral $ orig 83 | 84 | lookupSuffix :: RawFilePath -> GopherFileType 85 | lookupSuffix = fromMaybe File 86 | . (flip M.lookup) fileTypeMap 87 | . B.map asciiToLower 88 | 89 | data PathError 90 | = PathDoesNotExist 91 | | PathIsNotAllowed 92 | deriving (Show, Eq, Ord, Enum) 93 | 94 | -- | Action in the 'Either' monad which causes a 95 | -- failure if there's any dot files or directory 96 | -- in the given path 97 | checkNoDotFiles :: RawFilePath -> Either PathError () 98 | checkNoDotFiles path 99 | | containsDotFiles path = Left PathIsNotAllowed 100 | | otherwise = Right () 101 | 102 | -- | calculates the file type identifier used in the Gopher 103 | -- protocol for a given file and returns a descriptive error 104 | -- if the file is not accessible or a dot file (and thus not 105 | -- allowed to access) 106 | gopherFileType :: RawFilePath -> IO (Either PathError GopherFileType) 107 | gopherFileType path = (checkNoDotFiles path >>) <$> do 108 | let pathWide = decodeFilePath path 109 | isDir <- doesDirectoryExist pathWide 110 | if isDir 111 | then pure $ Right Directory 112 | else do 113 | fileExists <- doesFileExist pathWide 114 | pure $ 115 | if fileExists 116 | then Right $ lookupSuffix $ takeExtension path 117 | else Left $ PathDoesNotExist 118 | -------------------------------------------------------------------------------- /server/Network/Spacecookie/Path.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Network.Spacecookie.Path 3 | ( sanitizePath 4 | , makeAbsolute 5 | , containsDotFiles 6 | ) where 7 | 8 | import qualified Data.ByteString as B 9 | import System.FilePath.Posix.ByteString (RawFilePath, normalise, joinPath, splitPath, equalFilePath, ()) 10 | 11 | -- | Normalise a path and prevent . 12 | sanitizePath :: RawFilePath -> RawFilePath 13 | sanitizePath = 14 | joinPath 15 | . filter (\p -> not (equalFilePath p "..")) 16 | . splitPath . normalise 17 | 18 | -- | Convert a given path to an absolute path, treating it as if the current directory were the 19 | -- root directory. The result is 'normalise'd. Absolute paths are not changed (except for the 20 | -- normalisation). 21 | makeAbsolute :: RawFilePath -> RawFilePath 22 | makeAbsolute x = normalise $ "/" x 23 | 24 | -- | Wether any components of the given path begin with a dot, although @.@ is 25 | -- allowed. 26 | containsDotFiles :: RawFilePath -> Bool 27 | containsDotFiles = 28 | any (\p -> "." `B.isPrefixOf` p && not (equalFilePath p ".")) 29 | . splitPath 30 | -------------------------------------------------------------------------------- /server/Network/Spacecookie/Systemd.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | module Network.Spacecookie.Systemd 4 | ( systemdSocket 5 | , notifyReady 6 | , notifyStopping 7 | , systemdStoreOrClose 8 | , SystemdException (..) 9 | ) where 10 | 11 | import Control.Concurrent.MVar (newMVar, swapMVar, mkWeakMVar) 12 | import Control.Exception.Base 13 | import Control.Monad (when) 14 | import Data.Maybe (fromMaybe) 15 | import Foreign.C.Types (CInt (..)) 16 | import GHC.Conc (closeFdWith) 17 | import Network.Gopher 18 | import System.IO.Error (mkIOError, userErrorType) 19 | import System.Posix.Types (Fd (..)) 20 | import System.Socket hiding (Error (..)) 21 | import System.Socket.Family.Inet6 22 | import System.Socket.Type.Stream 23 | import System.Socket.Protocol.TCP 24 | import System.Socket.Unsafe (Socket (..)) 25 | import System.Systemd.Daemon (notifyReady, notifyStopping) 26 | import System.Systemd.Daemon.Fd (storeFd, getActivatedSockets) 27 | 28 | foreign import ccall unsafe "close" 29 | c_close :: CInt -> IO CInt 30 | 31 | -- | Close a 'Fd' using close(1). Throws an 'IOException' on error. 32 | closeFd :: Fd -> IO () 33 | closeFd fd = do 34 | res <- c_close $ fromIntegral fd 35 | when (res /= 0) $ throwIO 36 | $ mkIOError userErrorType "Could not close File Descriptor" Nothing Nothing 37 | 38 | -- | Irreversibly convert a 'Socket' into an 'Fd'. 39 | -- Invalidates the socket and returns the file descriptor 40 | -- contained within it. 41 | toFd :: Socket a b c -> IO Fd 42 | toFd (Socket mvar) = fmap (Fd . fromIntegral) (swapMVar mvar (-1)) 43 | -- putting an invalid file descriptor into the 'MVar' makes 44 | -- the 'Socket' appear to System.Socket as if it were closed 45 | 46 | -- | Create an 'Socket' from an 'Fd'. This action is unsafe 47 | -- since the type of the socket is not checked meaning that 48 | -- whatever type the resulting 'Socket' has is not guaranteed 49 | -- to be the same as its type indicates. Thus, this function 50 | -- needs to be used with care so the safety guarantees of 51 | -- 'Socket' are not violated. 52 | -- 53 | -- Throws an 'IOException' if the 'Fd' is invalid. 54 | fromFd :: Fd -> IO (Socket a b c) 55 | fromFd fd = do 56 | -- TODO Validate socket type 57 | when (fd < 0) $ throwIO 58 | $ mkIOError userErrorType "Invalid File Descriptor" Nothing Nothing 59 | mfd <- newMVar (fromIntegral fd) 60 | let s = Socket mfd 61 | _ <- mkWeakMVar mfd (close s) 62 | pure s 63 | 64 | data SystemdException 65 | = IncorrectNum 66 | deriving (Eq, Ord) 67 | 68 | instance Exception SystemdException 69 | instance Show SystemdException where 70 | show IncorrectNum = "SystemdException: Only exactly one Socket is supported" 71 | 72 | systemdSocket :: GopherConfig -> IO (Socket Inet6 Stream TCP) 73 | systemdSocket cfg = getActivatedSockets >>= \sockets -> 74 | case sockets of 75 | Nothing -> setupGopherSocket cfg 76 | Just [fd] -> do 77 | listenWarning 78 | fromFd fd 79 | Just _ -> throwIO IncorrectNum 80 | where listenWarning = fromMaybe (pure ()) $ do 81 | logAction <- cLogHandler cfg 82 | addr <- cListenAddr cfg 83 | pure . logAction GopherLogLevelWarn 84 | $ mconcat 85 | [ "Listen address ", toGopherLogStr addr 86 | , " specified, but started with systemd socket." 87 | , " Using systemd, listen address may differ." ] 88 | 89 | systemdStoreOrClose :: Socket Inet6 Stream TCP -> IO () 90 | systemdStoreOrClose s = do 91 | fd <- toFd s 92 | res <- storeFd fd 93 | case res of 94 | Just () -> return () 95 | Nothing -> closeFdWith closeFd fd 96 | -------------------------------------------------------------------------------- /spacecookie.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | name: spacecookie 3 | version: 1.0.0.3 4 | synopsis: Gopher server library and daemon 5 | description: Simple gopher library that allows writing custom gopher 6 | applications. Also includes a fully-featured gopher server 7 | daemon complete with gophermap-support built on top of it. 8 | license: GPL-3.0-only 9 | license-file: LICENSE 10 | author: Lukas Epple 11 | maintainer: sternenseemann@systemli.org 12 | category: Network 13 | build-type: Simple 14 | homepage: https://github.com/sternenseemann/spacecookie 15 | bug-reports: https://github.com/sternenseemann/spacecookie/issues 16 | extra-source-files: CHANGELOG.md 17 | README.md 18 | etc/spacecookie.json 19 | etc/spacecookie.service 20 | etc/spacecookie.socket 21 | docs/rfc1436.txt 22 | docs/man/spacecookie.1 23 | docs/man/spacecookie.json.5 24 | docs/man/spacecookie.gophermap.5 25 | test/data/pygopherd.gophermap 26 | test/data/bucktooth.gophermap 27 | test/integration/root/.gophermap 28 | test/integration/root/dir/.hidden 29 | test/integration/root/dir/another/.git-hello 30 | test/integration/root/dir/macintosh.hqx 31 | test/integration/root/dir/mystery-file 32 | test/integration/root/dir/strange.tXT 33 | test/integration/root/plain.txt 34 | test/integration/spacecookie.json 35 | 36 | common common-settings 37 | default-language: Haskell2010 38 | build-depends: base >=4.9 && <5 39 | , bytestring >= 0.10 40 | , attoparsec >= 0.13 41 | , directory >= 1.3 42 | , filepath-bytestring >=1.4 43 | , containers >= 0.6 44 | ghc-options: 45 | -Wall 46 | -Wno-orphans 47 | 48 | common common-executables 49 | ghc-options: 50 | -- Needed by curl to work reliably in the test suite 51 | -- https://github.com/GaloisInc/curl/pull/25 52 | -threaded 53 | -- Limit frequency of the idle GC to every 10s 54 | -rtsopts 55 | -with-rtsopts=-I10 56 | 57 | common gopher-dependencies 58 | build-depends: unix >= 2.7 59 | , socket >= 0.8.2 60 | , mtl >= 2.2 61 | , transformers >= 0.5 62 | , text >= 1.2 63 | , utf8-string >= 1.0 64 | 65 | executable spacecookie 66 | import: common-settings, common-executables, gopher-dependencies 67 | main-is: Main.hs 68 | build-depends: spacecookie 69 | , aeson >= 1.5 70 | , systemd >= 2.1.0 71 | , fast-logger >= 2.4.0 72 | hs-source-dirs: server 73 | other-modules: Network.Spacecookie.Config 74 | , Network.Spacecookie.Systemd 75 | , Network.Spacecookie.FileType 76 | , Network.Spacecookie.Path 77 | , Paths_spacecookie 78 | autogen-modules: Paths_spacecookie 79 | 80 | library 81 | import: common-settings, gopher-dependencies 82 | hs-source-dirs: src 83 | exposed-modules: Network.Gopher 84 | , Network.Gopher.Util.Gophermap 85 | other-modules: Network.Gopher.Types 86 | , Network.Gopher.Log 87 | , Network.Gopher.Util.Socket 88 | build-depends: async >= 2.2 89 | 90 | test-suite test 91 | import: common-settings, common-executables 92 | type: exitcode-stdio-1.0 93 | main-is: EntryPoint.hs 94 | hs-source-dirs: test, server 95 | other-modules: Test.FileTypeDetection 96 | , Test.Gophermap 97 | , Test.Integration 98 | , Test.Sanitization 99 | , Network.Spacecookie.FileType 100 | , Network.Spacecookie.Path 101 | build-depends: tasty >=1.2 102 | , tasty-hunit >=0.10 103 | , tasty-expected-failure >=0.11 104 | , spacecookie 105 | , process >=1.2.0 106 | , download-curl >=0.1 107 | , utf8-string >= 1.0 108 | 109 | source-repository head 110 | type: git 111 | location: https://github.com/sternenseemann/spacecookie.git 112 | 113 | source-repository head 114 | type: git 115 | location: https://code.sterni.lv/spacecookie 116 | -------------------------------------------------------------------------------- /spacecookie.nix: -------------------------------------------------------------------------------- 1 | { mkDerivation, aeson, async, attoparsec, base, bytestring 2 | , containers, directory, download-curl, fast-logger 3 | , filepath-bytestring, lib, mtl, process, socket, systemd, tasty 4 | , tasty-expected-failure, tasty-hunit, text, transformers, unix 5 | , utf8-string 6 | }: 7 | mkDerivation { 8 | pname = "spacecookie"; 9 | version = "1.0.0.3"; 10 | src = ./.; 11 | isLibrary = true; 12 | isExecutable = true; 13 | libraryHaskellDepends = [ 14 | async attoparsec base bytestring containers directory 15 | filepath-bytestring mtl socket text transformers unix utf8-string 16 | ]; 17 | executableHaskellDepends = [ 18 | aeson attoparsec base bytestring containers directory fast-logger 19 | filepath-bytestring mtl socket systemd text transformers unix 20 | utf8-string 21 | ]; 22 | testHaskellDepends = [ 23 | attoparsec base bytestring containers directory download-curl 24 | filepath-bytestring process tasty tasty-expected-failure 25 | tasty-hunit utf8-string 26 | ]; 27 | homepage = "https://github.com/sternenseemann/spacecookie"; 28 | description = "Gopher server library and daemon"; 29 | license = lib.licenses.gpl3Only; 30 | mainProgram = "spacecookie"; 31 | } 32 | -------------------------------------------------------------------------------- /src/Network/Gopher.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-| 3 | Module : Network.Gopher 4 | Stability : experimental 5 | Portability : POSIX 6 | 7 | = Overview 8 | 9 | This is the main module of the spacecookie library. 10 | It allows to write gopher applications by taking care of 11 | handling gopher requests while leaving the application 12 | logic to a user-supplied function. 13 | 14 | For a small tutorial an example of a trivial pure gopher application: 15 | 16 | @ 17 | {-# LANGUAGE OverloadedStrings #-} 18 | import "Network.Gopher" 19 | import "Network.Gopher.Util" 20 | 21 | cfg :: 'GopherConfig' 22 | cfg = 'defaultConfig' 23 | { cServerName = "localhost" 24 | , cServerPort = 7000 25 | } 26 | 27 | handler :: 'GopherRequest' -> 'GopherResponse' 28 | handler request = 29 | case 'requestSelector' request of 30 | "hello" -> 'FileResponse' "Hello, stranger!" 31 | "" -> rootMenu 32 | "/" -> rootMenu 33 | _ -> 'ErrorResponse' "Not found" 34 | where rootMenu = 'MenuResponse' 35 | [ 'Item' 'File' "greeting" "hello" Nothing Nothing ] 36 | 37 | main :: IO () 38 | main = 'runGopherPure' cfg handler 39 | @ 40 | 41 | There are three possibilities for a 'GopherResponse': 42 | 43 | * 'FileResponse': file type agnostic file response, takes a 44 | 'ByteString' to support both text and binary files. 45 | * 'MenuResponse': a gopher menu (“directory listing”) consisting of a 46 | list of 'GopherMenuItem's 47 | * 'ErrorResponse': gopher way to show an error (e. g. if a file is not found). 48 | An 'ErrorResponse' results in a menu response with a single entry. 49 | 50 | If you use 'runGopher', it is the same story like in the example above, but 51 | you can do 'IO' effects. To see a more elaborate example, have a look at the 52 | server code in this package. 53 | -} 54 | 55 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 56 | module Network.Gopher ( 57 | -- * Main API 58 | -- $runGopherVariants 59 | runGopher 60 | , runGopherPure 61 | , runGopherManual 62 | , GopherConfig (..) 63 | , defaultConfig 64 | -- ** Requests 65 | , GopherRequest (..) 66 | -- ** Responses 67 | , GopherResponse (..) 68 | , GopherMenuItem (..) 69 | , GopherFileType (..) 70 | -- * Helper Functions 71 | -- ** Logging 72 | -- $loggingDoc 73 | , GopherLogHandler 74 | , module Network.Gopher.Log 75 | -- ** Networking 76 | , setupGopherSocket 77 | -- ** Gophermaps 78 | -- $gophermapDoc 79 | , gophermapToDirectoryResponse 80 | , Gophermap 81 | , GophermapEntry (..) 82 | ) where 83 | 84 | import Prelude hiding (log) 85 | 86 | import Network.Gopher.Log 87 | import Network.Gopher.Types 88 | import Network.Gopher.Util.Gophermap 89 | import Network.Gopher.Util.Socket 90 | 91 | import Control.Concurrent (forkIO, ThreadId (), threadDelay) 92 | import Control.Concurrent.Async (race) 93 | import Control.Exception (bracket, catch, throw, SomeException (), Exception ()) 94 | import Control.Monad (forever, when, void) 95 | import Control.Monad.IO.Class (liftIO, MonadIO (..)) 96 | import Control.Monad.Reader (ask, runReaderT, MonadReader (..), ReaderT (..)) 97 | import Data.Bifunctor (second) 98 | import Data.ByteString (ByteString ()) 99 | import Data.Char (ord) 100 | import qualified Data.ByteString as B 101 | import qualified Data.ByteString.Builder as BB 102 | import qualified Data.ByteString.UTF8 as UTF8 103 | import Data.Maybe (fromMaybe) 104 | import Data.Word (Word8 (), Word16 ()) 105 | import System.Socket hiding (Error (..)) 106 | import System.Socket.Family.Inet6 107 | import System.Socket.Type.Stream (Stream, sendAllBuilder) 108 | import System.Socket.Protocol.TCP 109 | 110 | -- | Necessary information to handle gopher requests 111 | data GopherConfig 112 | = GopherConfig 113 | { cServerName :: ByteString 114 | -- ^ Public name of the server (either ip address or dns name). 115 | -- Gopher clients will use this name to fetch any resources 116 | -- listed in gopher menus located on the same server. 117 | , cListenAddr :: Maybe ByteString 118 | -- ^ Address or hostname to listen on (resolved by @getaddrinfo@). 119 | -- If 'Nothing', listen on all addresses. 120 | , cServerPort :: Integer 121 | -- ^ Port to listen on 122 | , cLogHandler :: Maybe GopherLogHandler 123 | -- ^ 'IO' action spacecookie will call to output its log messages. 124 | -- If it is 'Nothing', logging is disabled. See [the logging section](#logging) 125 | -- for an overview on how to implement a log handler. 126 | } 127 | 128 | -- | Default 'GopherConfig' describing a server on @localhost:70@ with 129 | -- no registered log handler. 130 | defaultConfig :: GopherConfig 131 | defaultConfig = GopherConfig "localhost" Nothing 70 Nothing 132 | 133 | -- | Type for an user defined 'IO' action which handles logging a 134 | -- given 'GopherLogStr' of a given 'GopherLogLevel'. It may 135 | -- process the string and format in any way desired, but it must 136 | -- be thread safe and should not block (too long) since it 137 | -- is called syncronously. 138 | type GopherLogHandler = GopherLogLevel -> GopherLogStr -> IO () 139 | 140 | -- $loggingDoc 141 | -- #logging# 142 | -- Logging may be enabled by providing 'GopherConfig' with an optional 143 | -- 'GopherLogHandler' which implements processing, formatting and 144 | -- outputting of log messages. While this requires extra work for the 145 | -- library user it also allows the maximum freedom in used logging 146 | -- mechanisms. 147 | -- 148 | -- A trivial log handler could look like this: 149 | -- 150 | -- @ 151 | -- logHandler :: 'GopherLogHandler' 152 | -- logHandler level str = do 153 | -- putStr $ show level ++ \": \" 154 | -- putStrLn $ 'fromGopherLogStr' str 155 | -- @ 156 | -- 157 | -- If you only want to log errors you can use the 'Ord' instance of 158 | -- 'GopherLogLevel': 159 | -- 160 | -- @ 161 | -- logHandler' :: 'GopherLogHandler' 162 | -- logHandler' level str = when (level <= 'GopherLogLevelError') 163 | -- $ logHandler level str 164 | -- @ 165 | -- 166 | -- The library marks parts of 'GopherLogStr' which contain user 167 | -- related data like IP addresses as sensitive using 'makeSensitive'. 168 | -- If you don't want to e. g. write personal information to disk in 169 | -- plain text, you can use 'hideSensitive' to transparently remove 170 | -- that information. Here's a quick example in GHCi: 171 | -- 172 | -- >>> hideSensitive $ "Look at my " <> makeSensitive "secret" 173 | -- "Look at my [redacted]" 174 | 175 | -- $gophermapDoc 176 | -- Helper functions for converting 'Gophermap's into 'MenuResponse's. 177 | -- For parsing gophermap files, refer to "Network.Gopher.Util.Gophermap". 178 | 179 | data GopherRequest 180 | = GopherRequest 181 | { requestRawSelector :: ByteString 182 | -- ^ raw selector sent by the client (without the terminating @\\r\\n@ 183 | , requestSelector :: ByteString 184 | -- ^ only the request selector minus the search expression if present 185 | , requestSearchString :: Maybe ByteString 186 | -- ^ raw search string if the clients sends a search transaction 187 | , requestClientAddr :: (Word16, Word16, Word16, Word16, Word16, Word16, Word16, Word16) 188 | -- ^ IPv6 address of the client which sent the request. IPv4 addresses are 189 | -- 190 | -- to an IPv6 address. 191 | } deriving (Show, Eq) 192 | 193 | data Env 194 | = Env 195 | { serverConfig :: GopherConfig 196 | , serverFun :: GopherRequest -> IO GopherResponse 197 | } 198 | 199 | newtype GopherM a = GopherM { runGopherM :: ReaderT Env IO a } 200 | deriving (Functor, Applicative, Monad, MonadIO, MonadReader Env) 201 | 202 | gopherM :: Env -> GopherM a -> IO a 203 | gopherM env action = (runReaderT . runGopherM) action env 204 | 205 | -- call given log handler if it is Just 206 | logIO :: Maybe GopherLogHandler -> GopherLogLevel -> GopherLogStr -> IO () 207 | logIO h l = fromMaybe (const (pure ())) $ ($ l) <$> h 208 | 209 | logInfo :: GopherLogStr -> GopherM () 210 | logInfo = log GopherLogLevelInfo 211 | 212 | logError :: GopherLogStr -> GopherM () 213 | logError = log GopherLogLevelError 214 | 215 | log :: GopherLogLevel -> GopherLogStr -> GopherM () 216 | log l m = do 217 | h <- cLogHandler . serverConfig <$> ask 218 | liftIO $ logIO h l m 219 | 220 | logException :: Exception e => Maybe GopherLogHandler -> GopherLogStr -> e -> IO () 221 | logException logger msg e = 222 | logIO logger GopherLogLevelError $ msg <> toGopherLogStr (show e) 223 | 224 | -- | Read request from a client socket. 225 | -- The complexity of this function is caused by the 226 | -- following design features: 227 | -- 228 | -- * Requests may be terminated by either "\n\r" or "\n" 229 | -- * After the terminating newline no extra data is accepted 230 | -- * Give up on waiting on a request from the client after 231 | -- a certain amount of time (request timeout) 232 | -- * Don't accept selectors bigger than a certain size to 233 | -- avoid DoS attacks filling up our memory. 234 | receiveRequest :: Socket Inet6 Stream TCP -> IO (Either ByteString ByteString) 235 | receiveRequest sock = fmap (either id id) 236 | $ race (threadDelay reqTimeout >> pure (Left "Request Timeout")) $ do 237 | req <- loop mempty 0 238 | pure $ 239 | case B.break newline req of 240 | (r, "\r\n") -> Right r 241 | (r, "\n") -> Right r 242 | (_, "") -> Left "Request too big or unterminated" 243 | _ -> Left "Unexpected data after newline" 244 | where cr, lf :: Word8 245 | lf = fromIntegral $ ord '\n' 246 | cr = fromIntegral $ ord '\r' 247 | newline = (||) <$> (== lf) <*> (== cr) 248 | reqTimeout = 10000000 -- 10s 249 | maxSize = 1024 * 1024 250 | loop bs size = do 251 | part <- receive sock maxSize msgNoSignal 252 | let newSize = size + B.length part 253 | if newSize >= maxSize || part == mempty || B.elem lf part 254 | then pure $ bs `mappend` part 255 | else loop (bs `mappend` part) newSize 256 | 257 | -- | Auxiliary function that sets up the listening socket for 258 | -- 'runGopherManual' correctly and starts to listen. 259 | -- 260 | -- May throw a 'SocketException' if an error occurs while 261 | -- setting up the socket. 262 | setupGopherSocket :: GopherConfig -> IO (Socket Inet6 Stream TCP) 263 | setupGopherSocket cfg = do 264 | sock <- (socket :: IO (Socket Inet6 Stream TCP)) 265 | setSocketOption sock (ReuseAddress True) 266 | setSocketOption sock (V6Only False) 267 | addr <- 268 | case cListenAddr cfg of 269 | Nothing -> pure 270 | $ SocketAddressInet6 inet6Any (fromInteger (cServerPort cfg)) 0 0 271 | Just a -> do 272 | let port = UTF8.fromString . show $ cServerPort cfg 273 | let flags = aiV4Mapped <> aiNumericService 274 | addrs <- (getAddressInfo (Just a) (Just port) flags :: IO [AddressInfo Inet6 Stream TCP]) 275 | 276 | -- should be done by getAddressInfo already 277 | when (null addrs) $ throw eaiNoName 278 | 279 | pure . socketAddress $ head addrs 280 | bind sock addr 281 | listen sock 5 282 | pure sock 283 | 284 | -- $runGopherVariants 285 | -- The @runGopher@ function variants will generally not throw exceptions, 286 | -- but handle them somehow (usually by logging that a non-fatal exception 287 | -- occurred) except if the exception occurrs in the setup step of 288 | -- 'runGopherManual'. 289 | -- 290 | -- You'll have to handle those exceptions yourself. To see which exceptions 291 | -- can be thrown by 'runGopher' and 'runGopherPure', read the documentation 292 | -- of 'setupGopherSocket'. 293 | 294 | -- | Run a gopher application that may cause effects in 'IO'. 295 | -- The application function is given the 'GopherRequest' 296 | -- sent by the client and must produce a GopherResponse. 297 | runGopher :: GopherConfig -> (GopherRequest -> IO GopherResponse) -> IO () 298 | runGopher cfg f = runGopherManual (setupGopherSocket cfg) (pure ()) close cfg f 299 | 300 | -- | Same as 'runGopher', but allows you to setup the 'Socket' manually 301 | -- and calls an user provided action soon as the server is ready 302 | -- to accept requests. When the server terminates, it calls the given 303 | -- clean up action which must close the socket and may perform other 304 | -- shutdown tasks (like notifying a supervisor it is stopping). 305 | -- 306 | -- Spacecookie assumes the 'Socket' is properly set up to listen on the 307 | -- port and host specified in the 'GopherConfig' (i. e. 'bind' and 308 | -- 'listen' have been called). This can be achieved using 'setupGopherSocket'. 309 | -- Especially note that spacecookie does /not/ check if the listening 310 | -- address and port of the given socket match 'cListenAddr' and 311 | -- 'cServerPort'. 312 | -- 313 | -- This is intended for supporting systemd socket activation and storage, 314 | -- but may also be used to support other use cases where more control is 315 | -- necessary. Always use 'runGopher' if possible, as it offers less ways 316 | -- of messing things up. 317 | runGopherManual :: IO (Socket Inet6 Stream TCP) -- ^ action to set up listening socket 318 | -> IO () -- ^ ready action called after startup 319 | -> (Socket Inet6 Stream TCP -> IO ()) -- ^ socket clean up action 320 | -> GopherConfig -- ^ server config 321 | -> (GopherRequest -> IO GopherResponse) -- ^ request handler 322 | -> IO () 323 | runGopherManual sockAction ready term cfg f = bracket 324 | sockAction 325 | term 326 | (\sock -> do 327 | gopherM (Env cfg f) $ do 328 | addr <- liftIO $ getAddress sock 329 | logInfo $ "Listening on " <> toGopherLogStr addr 330 | 331 | liftIO $ ready 332 | 333 | forever $ acceptAndHandle sock) 334 | 335 | forkGopherM :: GopherM () -> IO () -> GopherM ThreadId 336 | forkGopherM action cleanup = do 337 | env <- ask 338 | liftIO $ forkIO $ do 339 | gopherM env action `catch` 340 | (logException 341 | (cLogHandler $ serverConfig env) 342 | "Thread failed with exception: " :: SomeException -> IO ()) 343 | cleanup 344 | 345 | -- | Split an selector in the actual search selector and 346 | -- an optional search expression as documented in the 347 | -- RFC1436 appendix. 348 | splitSelector :: ByteString -> (ByteString, Maybe ByteString) 349 | splitSelector = second checkSearch . B.breakSubstring "\t" 350 | where checkSearch search = 351 | if B.length search > 1 352 | then Just $ B.tail search 353 | else Nothing 354 | 355 | handleIncoming :: Socket Inet6 Stream TCP -> SocketAddress Inet6 -> GopherM () 356 | handleIncoming clientSock addr@(SocketAddressInet6 cIpv6 _ _ _) = do 357 | request <- liftIO $ receiveRequest clientSock 358 | logger <- cLogHandler . serverConfig <$> ask 359 | intermediateResponse <- 360 | case request of 361 | Left e -> pure $ ErrorResponse e 362 | Right rawSelector -> do 363 | let (onlySel, search) = splitSelector rawSelector 364 | req = GopherRequest 365 | { requestRawSelector = rawSelector 366 | , requestSelector = onlySel 367 | , requestSearchString = search 368 | , requestClientAddr = inet6AddressToTuple cIpv6 369 | } 370 | 371 | logInfo $ "New Request \"" <> toGopherLogStr rawSelector <> "\" from " 372 | <> makeSensitive (toGopherLogStr addr) 373 | 374 | fun <- serverFun <$> ask 375 | liftIO $ fun req `catch` \e -> do 376 | let msg = "Unhandled exception in handler: " 377 | <> toGopherLogStr (show (e :: SomeException)) 378 | logIO logger GopherLogLevelError msg 379 | pure $ ErrorResponse "Unknown error occurred" 380 | 381 | rawResponse <- response intermediateResponse 382 | 383 | liftIO $ void (sendAllBuilder clientSock 10240 rawResponse msgNoSignal) `catch` \e -> 384 | logException logger "Exception while sending response to client: " (e :: SocketException) 385 | 386 | acceptAndHandle :: Socket Inet6 Stream TCP -> GopherM () 387 | acceptAndHandle sock = do 388 | connection <- liftIO $ fmap Right (accept sock) `catch` (pure . Left) 389 | case connection of 390 | Left e -> logError $ "Failure while accepting connection " 391 | <> toGopherLogStr (show (e :: SocketException)) 392 | Right (clientSock, addr) -> do 393 | logInfo $ "New connection from " <> makeSensitive (toGopherLogStr addr) 394 | void $ forkGopherM (handleIncoming clientSock addr) (gracefulClose clientSock) 395 | 396 | -- | Like 'runGopher', but may not cause effects in 'IO' (or anywhere else). 397 | runGopherPure :: GopherConfig -> (GopherRequest -> GopherResponse) -> IO () 398 | runGopherPure cfg f = runGopher cfg (fmap pure f) 399 | 400 | response :: GopherResponse -> GopherM BB.Builder 401 | response (FileResponse str) = pure $ BB.byteString str 402 | response (ErrorResponse reason) = response . MenuResponse $ 403 | [ Item Error reason "Err" Nothing Nothing ] 404 | response (MenuResponse items) = 405 | let appendItem cfg acc (Item fileType title path host port) = 406 | acc <> BB.word8 (fileTypeToChar fileType) <> mconcat 407 | [ BB.byteString title 408 | , BB.charUtf8 '\t' 409 | , BB.byteString path 410 | , BB.charUtf8 '\t' 411 | , BB.byteString $ fromMaybe (cServerName cfg) host 412 | , BB.charUtf8 '\t' 413 | , BB.intDec . fromIntegral $ fromMaybe (cServerPort cfg) port 414 | , BB.byteString "\r\n" 415 | ] 416 | in do 417 | cfg <- serverConfig <$> ask 418 | pure $ foldl (appendItem cfg) mempty items 419 | -------------------------------------------------------------------------------- /src/Network/Gopher/Log.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | -- | This module is completely exposed by 'Network.Gopher' 4 | module Network.Gopher.Log 5 | ( GopherLogStr () 6 | , makeSensitive 7 | , hideSensitive 8 | , GopherLogLevel (..) 9 | , ToGopherLogStr (..) 10 | , FromGopherLogStr (..) 11 | ) where 12 | 13 | import Data.ByteString.Builder (Builder ()) 14 | import qualified Data.ByteString as B 15 | import qualified Data.ByteString.Lazy as BL 16 | import qualified Data.ByteString.Builder as BB 17 | import qualified Data.ByteString.UTF8 as UTF8 18 | import qualified Data.Sequence as S 19 | import Data.String (IsString (..)) 20 | import qualified Data.Text as T 21 | import qualified Data.Text.Encoding as T 22 | import qualified Data.Text.Encoding.Error as T 23 | import qualified Data.Text.Lazy as TL 24 | import qualified Data.Text.Lazy.Encoding as TL 25 | import System.Socket.Family.Inet6 26 | 27 | -- | Indicates the log level of a 'GopherLogStr' to a 28 | -- 'Network.Gopher.GopherLogHandler'. If you want to 29 | -- filter by log level you can use either the 'Ord' 30 | -- or 'Enum' instance of 'GopherLogLevel' as the following 31 | -- holds: 32 | -- 33 | -- @ 34 | -- 'GopherLogLevelError' < 'GopherLogLevelWarn' < 'GopherLogLevelInfo' 35 | -- @ 36 | data GopherLogLevel 37 | = GopherLogLevelError 38 | | GopherLogLevelWarn 39 | | GopherLogLevelInfo 40 | deriving (Show, Eq, Ord, Enum) 41 | 42 | -- | UTF-8 encoded string which may have parts of it marked as 43 | -- sensitive (see 'makeSensitive'). Use its 'ToGopherLogStr', 44 | -- 'Semigroup' and 'IsString' instances to construct 45 | -- 'GopherLogStr's and 'FromGopherLogStr' to convert to the 46 | -- commonly used Haskell string types. 47 | -- 48 | -- Note that encoding isn't checked for conversions from 49 | -- encoding agnostic types, e.g. 'BB.ByteString'. 50 | -- In 'FromGopherLogStr', invalid encoding is retained when converting to 51 | -- encoding agnostic types (e.g. 'BB.ByteString'), but will be replaced by 52 | -- replacement characters for types that enforce encoding (e.g. 'T.Text'). 53 | newtype GopherLogStr 54 | = GopherLogStr { unGopherLogStr :: S.Seq GopherLogStrChunk } 55 | 56 | instance Show GopherLogStr where 57 | show = show . (fromGopherLogStr :: GopherLogStr -> String) 58 | 59 | instance Semigroup GopherLogStr where 60 | GopherLogStr s1 <> GopherLogStr s2 = GopherLogStr (s1 <> s2) 61 | 62 | instance Monoid GopherLogStr where 63 | mempty = GopherLogStr mempty 64 | 65 | instance IsString GopherLogStr where 66 | fromString = toGopherLogStr 67 | 68 | data GopherLogStrChunk 69 | = GopherLogStrChunk 70 | { glscSensitive :: Bool 71 | , glscBuilder :: Builder 72 | } 73 | 74 | -- | Mark a 'GopherLogStr' as sensitive. This is used by this 75 | -- library mostly to mark IP addresses of connecting clients. 76 | -- By using 'hideSensitive' on a 'GopherLogStr' sensitive 77 | -- parts will be hidden from the string — even if the sensitive 78 | -- string was concatenated to other strings. 79 | makeSensitive :: GopherLogStr -> GopherLogStr 80 | makeSensitive = GopherLogStr 81 | . fmap (\c -> c { glscSensitive = True }) 82 | . unGopherLogStr 83 | 84 | -- | Replaces all chunks of the 'GopherLogStr' that have been 85 | -- marked as sensitive by 'makeSensitive' with @[redacted]@. 86 | -- Note that the chunking is dependent on the way the string 87 | -- was assembled by the user and the internal implementation 88 | -- of 'GopherLogStr' which can lead to multiple consecutive 89 | -- @[redacted]@ being returned unexpectedly. This may be 90 | -- improved in the future. 91 | hideSensitive :: GopherLogStr -> GopherLogStr 92 | hideSensitive = GopherLogStr 93 | . fmap (\c -> GopherLogStrChunk False $ 94 | if glscSensitive c 95 | then BB.byteString "[redacted]" 96 | else glscBuilder c) 97 | . unGopherLogStr 98 | 99 | -- | Convert 'GopherLogStr's to other string types. Since it is used 100 | -- internally by 'GopherLogStr', it is best to use the 'Builder' 101 | -- instance for performance if possible. 102 | class FromGopherLogStr a where 103 | fromGopherLogStr :: GopherLogStr -> a 104 | 105 | instance FromGopherLogStr GopherLogStr where 106 | fromGopherLogStr = id 107 | 108 | instance FromGopherLogStr Builder where 109 | fromGopherLogStr = foldMap glscBuilder . unGopherLogStr 110 | 111 | instance FromGopherLogStr BL.ByteString where 112 | fromGopherLogStr = BB.toLazyByteString . fromGopherLogStr 113 | 114 | instance FromGopherLogStr B.ByteString where 115 | fromGopherLogStr = BL.toStrict . fromGopherLogStr 116 | 117 | -- | Any non-UTF-8 portions (introduced e.g. via @'ToGopherLogStr' 118 | -- 'BB.ByteString'@) are replaced with U+FFFD. 119 | instance FromGopherLogStr T.Text where 120 | -- text >= 2.0 introduces a shortcut for this, but we support a wider range 121 | fromGopherLogStr = T.decodeUtf8With T.lenientDecode . fromGopherLogStr 122 | 123 | -- | Any non-UTF-8 portions (introduced e.g. via @'ToGopherLogStr' 124 | -- 'BB.ByteString'@) are replaced with U+FFFD. 125 | instance FromGopherLogStr TL.Text where 126 | fromGopherLogStr = TL.decodeUtf8With T.lenientDecode . fromGopherLogStr 127 | 128 | -- | Any non-UTF-8 portions (introduced e.g. via @'ToGopherLogStr' 129 | -- 'BB.ByteString'@) are replaced with U+FFFD. 130 | instance FromGopherLogStr [Char] where 131 | fromGopherLogStr = UTF8.toString . fromGopherLogStr 132 | 133 | -- | Convert something to a 'GopherLogStr'. In terms of 134 | -- performance it is best to implement a 'Builder' for 135 | -- the type you are trying to render to 'GopherLogStr' 136 | -- and then reuse its 'ToGopherLogStr' instance. 137 | class ToGopherLogStr a where 138 | toGopherLogStr :: a -> GopherLogStr 139 | 140 | instance ToGopherLogStr GopherLogStr where 141 | toGopherLogStr = id 142 | 143 | -- | UTF-8 encoding is not checked, needs to be ensured by the user. 144 | instance ToGopherLogStr Builder where 145 | toGopherLogStr b = GopherLogStr 146 | . S.singleton 147 | $ GopherLogStrChunk 148 | { glscSensitive = False 149 | , glscBuilder = b 150 | } 151 | 152 | -- | UTF-8 encoding is not checked, needs to be ensured by the user. 153 | instance ToGopherLogStr B.ByteString where 154 | toGopherLogStr = toGopherLogStr . BB.byteString 155 | 156 | -- | UTF-8 encoding is not checked, needs to be ensured by the user. 157 | instance ToGopherLogStr BL.ByteString where 158 | toGopherLogStr = toGopherLogStr . BB.lazyByteString 159 | 160 | instance ToGopherLogStr [Char] where 161 | toGopherLogStr = toGopherLogStr . UTF8.fromString 162 | 163 | instance ToGopherLogStr GopherLogLevel where 164 | toGopherLogStr l = 165 | case l of 166 | GopherLogLevelInfo -> toGopherLogStr ("info" :: B.ByteString) 167 | GopherLogLevelWarn -> toGopherLogStr ("warn" :: B.ByteString) 168 | GopherLogLevelError -> toGopherLogStr ("error" :: B.ByteString) 169 | 170 | instance ToGopherLogStr (SocketAddress Inet6) where 171 | -- TODO shorten address if possible 172 | toGopherLogStr (SocketAddressInet6 addr port _ _) = 173 | let (b1, b2, b3, b4, b5, b6, b7, b8) = inet6AddressToTuple addr 174 | in toGopherLogStr $ 175 | BB.charUtf8 '[' <> 176 | BB.word16HexFixed b1 <> BB.charUtf8 ':' <> 177 | BB.word16HexFixed b2 <> BB.charUtf8 ':' <> 178 | BB.word16HexFixed b3 <> BB.charUtf8 ':' <> 179 | BB.word16HexFixed b4 <> BB.charUtf8 ':' <> 180 | BB.word16HexFixed b5 <> BB.charUtf8 ':' <> 181 | BB.word16HexFixed b6 <> BB.charUtf8 ':' <> 182 | BB.word16HexFixed b7 <> BB.charUtf8 ':' <> 183 | BB.word16HexFixed b8 <> BB.charUtf8 ']' <> 184 | BB.charUtf8 ':' <> BB.intDec (fromIntegral port) 185 | -------------------------------------------------------------------------------- /src/Network/Gopher/Types.hs: -------------------------------------------------------------------------------- 1 | module Network.Gopher.Types 2 | ( GopherFileType (..) 3 | , GopherResponse (..) 4 | , GopherMenuItem (..) 5 | , fileTypeToChar 6 | , charToFileType 7 | , isFile 8 | ) 9 | where 10 | 11 | import Prelude hiding (lookup) 12 | 13 | import Data.ByteString (ByteString ()) 14 | import Data.Char (chr, ord) 15 | import Data.Word (Word8 ()) 16 | 17 | -- | entry in a gopher menu 18 | data GopherMenuItem 19 | = Item GopherFileType ByteString ByteString (Maybe ByteString) (Maybe Integer) 20 | -- ^ file type, menu text, selector, server name (optional), port (optional). 21 | -- None of the given 'ByteString's may contain tab characters. 22 | deriving (Show, Eq) 23 | 24 | data GopherResponse 25 | = MenuResponse [GopherMenuItem] -- ^ gopher menu, wrapper around a list of 'GopherMenuItem's 26 | | FileResponse ByteString -- ^ return the given 'ByteString' as a file 27 | | ErrorResponse ByteString -- ^ gopher menu containing a single error with the given 'ByteString' as text 28 | deriving (Show, Eq) 29 | 30 | -- | rfc-defined gopher file types plus info line and HTML 31 | data GopherFileType 32 | = File -- ^ text file, default type 33 | | Directory -- ^ a gopher menu 34 | | PhoneBookServer 35 | | Error -- ^ error entry in menu 36 | | BinHexMacintoshFile 37 | | DOSArchive 38 | | UnixUuencodedFile 39 | | IndexSearchServer 40 | | TelnetSession 41 | | BinaryFile -- ^ binary file 42 | | RedundantServer 43 | | Tn3270Session 44 | | GifFile -- ^ gif 45 | | ImageFile -- ^ image of any format 46 | | InfoLine -- ^ menu entry without associated file 47 | | Html -- ^ Special type for HTML, most commonly used for 48 | deriving (Show, Eq, Ord, Enum) 49 | 50 | fileTypeToChar :: GopherFileType -> Word8 51 | fileTypeToChar t = fromIntegral . ord $ 52 | case t of 53 | File -> '0' 54 | Directory -> '1' 55 | PhoneBookServer -> '2' 56 | Error -> '3' 57 | BinHexMacintoshFile -> '4' 58 | DOSArchive -> '5' 59 | UnixUuencodedFile -> '6' 60 | IndexSearchServer -> '7' 61 | TelnetSession -> '8' 62 | BinaryFile -> '9' 63 | RedundantServer -> '+' 64 | Tn3270Session -> 'T' 65 | GifFile -> 'g' 66 | ImageFile -> 'I' 67 | InfoLine -> 'i' 68 | Html -> 'h' 69 | 70 | charToFileType :: Word8 -> GopherFileType 71 | charToFileType c = 72 | case chr (fromIntegral c) of 73 | '0' -> File 74 | '1' -> Directory 75 | '2' -> PhoneBookServer 76 | '3' -> Error 77 | '4' -> BinHexMacintoshFile 78 | '5' -> DOSArchive 79 | '6' -> UnixUuencodedFile 80 | '7' -> IndexSearchServer 81 | '8' -> TelnetSession 82 | '9' -> BinaryFile 83 | '+' -> RedundantServer 84 | 'T' -> Tn3270Session 85 | 'g' -> GifFile 86 | 'I' -> ImageFile 87 | 'i' -> InfoLine 88 | 'h' -> Html 89 | _ -> InfoLine -- default value 90 | 91 | isFile :: GopherFileType -> Bool 92 | isFile File = True 93 | isFile BinHexMacintoshFile = True 94 | isFile DOSArchive = True 95 | isFile UnixUuencodedFile = True 96 | isFile GifFile = True 97 | isFile ImageFile = True 98 | isFile _ = False 99 | -------------------------------------------------------------------------------- /src/Network/Gopher/Util/Gophermap.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Network.Gopher.Util.Gophermap 3 | Stability : experimental 4 | Portability : POSIX 5 | 6 | This module implements a parser for gophermap files. 7 | 8 | Example usage: 9 | 10 | @ 11 | import Network.Gopher.Util.Gophermap 12 | import qualified Data.ByteString as B 13 | import Data.Attoparsec.ByteString 14 | 15 | main = do 16 | file <- B.readFile "gophermap" 17 | print $ parseOnly parseGophermap file 18 | @ 19 | 20 | 21 | -} 22 | 23 | {-# LANGUAGE OverloadedStrings #-} 24 | module Network.Gopher.Util.Gophermap ( 25 | parseGophermap 26 | , GophermapEntry (..) 27 | , GophermapFilePath (..) 28 | , Gophermap 29 | , gophermapToDirectoryResponse 30 | ) where 31 | 32 | import Prelude hiding (take, takeWhile) 33 | 34 | import Network.Gopher.Types 35 | 36 | import Control.Applicative ((<|>)) 37 | import Data.Attoparsec.ByteString 38 | import Data.Attoparsec.ByteString.Char8 (isDigit_w8) 39 | import Data.ByteString (ByteString (), pack, unpack, isPrefixOf) 40 | import Data.Char (chr) 41 | import Data.Maybe (fromMaybe) 42 | import Data.Word (Word8 ()) 43 | import System.FilePath.Posix.ByteString (RawFilePath, (), isAbsolute, normalise) 44 | import Text.Read (readEither) 45 | 46 | -- | Given a directory and a Gophermap contained within it, 47 | -- return the corresponding gopher menu response. 48 | gophermapToDirectoryResponse :: RawFilePath -> Gophermap -> GopherResponse 49 | gophermapToDirectoryResponse dir entries = 50 | MenuResponse (map (gophermapEntryToMenuItem dir) entries) 51 | 52 | gophermapEntryToMenuItem :: RawFilePath -> GophermapEntry -> GopherMenuItem 53 | gophermapEntryToMenuItem dir (GophermapEntry ft desc path host port) = 54 | Item ft desc (fromMaybe desc (realPath <$> path)) host port 55 | where realPath p = 56 | case p of 57 | GophermapAbsolute p' -> p' 58 | -- TODO: `..` should be resolved textually for linking to the 59 | -- parent directory (if possible) 60 | GophermapRelative p' -> dir p' 61 | GophermapUrl u -> u 62 | 63 | fileTypeChars :: [Char] 64 | fileTypeChars = "0123456789+TgIih" 65 | 66 | -- | Wrapper around 'RawFilePath' to indicate whether it is 67 | -- relative or absolute. 68 | data GophermapFilePath 69 | = GophermapAbsolute RawFilePath -- ^ Absolute path starting with @/@ 70 | | GophermapRelative RawFilePath -- ^ Relative path 71 | | GophermapUrl RawFilePath -- ^ URL to another protocol starting with @URL:@ 72 | deriving (Show, Eq) 73 | 74 | -- | Take selector 'ByteString' from gophermap and 75 | -- determine its 'GophermapFilePath' type. 76 | -- Relative and absolute paths are 'normalised', 77 | -- URLs passed on as is. 78 | -- 79 | -- * Selectors that start with @"URL:"@ are considered 80 | -- an external URL and left as-is. 81 | -- * Absolute paths are identified by 'isAbsolute'. 82 | -- * Everything else is considered a relative path. 83 | -- 84 | -- Paths are 'normalise'-d, but not subject to any other 85 | -- processing. 86 | makeGophermapFilePath :: ByteString -> GophermapFilePath 87 | makeGophermapFilePath b 88 | | "URL:" `isPrefixOf` b = GophermapUrl b 89 | | isAbsolute b = GophermapAbsolute normalisedPath 90 | | otherwise = GophermapRelative normalisedPath 91 | where 92 | normalisedPath = normalise b 93 | 94 | -- | A gophermap entry makes all values of a gopher menu item optional except for file type and description. When converting to a 'GopherMenuItem', appropriate default values are used. 95 | data GophermapEntry = GophermapEntry 96 | GopherFileType ByteString 97 | (Maybe GophermapFilePath) (Maybe ByteString) (Maybe Integer) -- ^ file type, description, path, server name, port number 98 | deriving (Show, Eq) 99 | 100 | type Gophermap = [GophermapEntry] 101 | 102 | -- | Attoparsec 'Parser' for the gophermap file format 103 | parseGophermap :: Parser Gophermap 104 | parseGophermap = many1 parseGophermapLine <* endOfInput 105 | 106 | gopherFileTypeChar :: Parser Word8 107 | gopherFileTypeChar = satisfy (inClass fileTypeChars) 108 | 109 | parseGophermapLine :: Parser GophermapEntry 110 | parseGophermapLine = emptyGophermapline 111 | <|> regularGophermapline 112 | <|> infoGophermapline 113 | 114 | infoGophermapline :: Parser GophermapEntry 115 | infoGophermapline = do 116 | text <- takeWhile1 (notInClass "\t\r\n") 117 | endOfLineOrInput 118 | return $ GophermapEntry InfoLine 119 | text 120 | Nothing 121 | Nothing 122 | Nothing 123 | 124 | regularGophermapline :: Parser GophermapEntry 125 | regularGophermapline = do 126 | fileTypeChar <- gopherFileTypeChar 127 | text <- itemValue 128 | _ <- satisfy (inClass "\t") 129 | pathString <- option Nothing $ Just <$> itemValue 130 | host <- optionalValue 131 | port <- optional portValue 132 | endOfLineOrInput 133 | return $ GophermapEntry (charToFileType fileTypeChar) 134 | text 135 | (makeGophermapFilePath <$> pathString) 136 | host 137 | port 138 | 139 | emptyGophermapline :: Parser GophermapEntry 140 | emptyGophermapline = do 141 | endOfLine' 142 | return emptyInfoLine 143 | where emptyInfoLine = GophermapEntry InfoLine (pack []) Nothing Nothing Nothing 144 | 145 | portValue :: Parser Integer 146 | portValue = do 147 | digits <- takeWhile1 isDigit_w8 148 | -- we know digits is just ASCII characters ([0-9]) 149 | case readEither (map (chr . fromIntegral) (unpack digits)) of 150 | Left e -> fail e 151 | Right p -> pure p 152 | 153 | optionalValue :: Parser (Maybe ByteString) 154 | optionalValue = optional itemValue 155 | 156 | optional :: Parser a -> Parser (Maybe a) 157 | optional parser = option Nothing $ do 158 | _ <- satisfy (inClass "\t") 159 | Just <$> parser 160 | 161 | itemValue :: Parser ByteString 162 | itemValue = takeWhile1 (notInClass "\t\r\n") 163 | 164 | endOfLine' :: Parser () 165 | endOfLine' = (word8 10 >> return ()) <|> (string "\r\n" >> return ()) 166 | 167 | endOfLineOrInput :: Parser () 168 | endOfLineOrInput = endOfInput <|> endOfLine' 169 | -------------------------------------------------------------------------------- /src/Network/Gopher/Util/Socket.hs: -------------------------------------------------------------------------------- 1 | -- | Internal socket utilities implementing missing 2 | -- features of 'System.Socket' which are yet to be 3 | -- upstreamed. 4 | module Network.Gopher.Util.Socket 5 | ( gracefulClose 6 | ) where 7 | 8 | import Control.Concurrent.MVar (withMVar) 9 | import Control.Concurrent (threadDelay) 10 | import Control.Concurrent.Async (race) 11 | import Control.Exception.Base (throwIO) 12 | import Control.Monad (void, when) 13 | import Data.Functor ((<&>)) 14 | import Foreign.C.Error (Errno (..), getErrno) 15 | import Foreign.C.Types (CInt (..)) 16 | import System.Socket (receive, msgNoSignal, SocketException (..), close, Family ()) 17 | import System.Socket.Type.Stream (Stream ()) 18 | import System.Socket.Protocol.TCP (TCP ()) 19 | import System.Socket.Unsafe (Socket (..)) 20 | 21 | -- Until https://github.com/lpeterse/haskell-socket/pull/67 gets 22 | -- merged, we have to implement shutdown ourselves. 23 | foreign import ccall unsafe "shutdown" 24 | c_shutdown :: CInt -> CInt -> IO CInt 25 | 26 | data ShutdownHow 27 | -- | Disallow Reading (calls to 'receive' are empty). 28 | = ShutdownRead 29 | -- | Disallow Writing (calls to 'send' throw). 30 | | ShutdownWrite 31 | -- | Disallow both. 32 | | ShutdownReadWrite 33 | deriving (Show, Eq, Ord, Enum) 34 | 35 | -- | Shutdown a stream connection (partially). 36 | -- Will send TCP FIN and prompt a client to 37 | -- close the connection. 38 | -- 39 | -- Not exposed to prevent future name clash. 40 | shutdown :: Socket a Stream TCP -> ShutdownHow -> IO () 41 | shutdown (Socket mvar) how = withMVar mvar $ \fd -> do 42 | res <- c_shutdown (fromIntegral fd) 43 | $ fromIntegral $ fromEnum how 44 | when (res /= 0) $ throwIO =<< 45 | (getErrno <&> \(Errno errno) -> SocketException errno) 46 | 47 | -- | Shutdown connection and give client a bit 48 | -- of time to clean up on its end before closing 49 | -- the connection to avoid a broken pipe on the 50 | -- other side. 51 | gracefulClose :: Family f => Socket f Stream TCP -> IO () 52 | gracefulClose sock = do 53 | -- send TCP FIN 54 | shutdown sock ShutdownWrite 55 | -- wait for some kind of read from the 56 | -- client (either mempty, meaning TCP FIN, 57 | -- something else which would mean protocol 58 | -- violation). Give up after 1s. 59 | _ <- race (void $ receive sock 16 msgNoSignal) (threadDelay 1000000) 60 | close sock 61 | -------------------------------------------------------------------------------- /test/EntryPoint.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Test.Tasty 4 | 5 | -- library tests 6 | import Test.Gophermap 7 | -- library-ish 8 | import Test.Sanitization 9 | 10 | -- server executable tests 11 | import Test.FileTypeDetection 12 | import Test.Integration 13 | 14 | main :: IO () 15 | main = defaultMain tests 16 | 17 | tests :: TestTree 18 | tests = testGroup "tests" 19 | [ gophermapTests 20 | , sanitizationTests 21 | , fileTypeDetectionTests 22 | , integrationTests 23 | ] 24 | -------------------------------------------------------------------------------- /test/Test/FileTypeDetection.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Test.FileTypeDetection (fileTypeDetectionTests) where 3 | 4 | import Network.Gopher (GopherFileType (..)) 5 | import Network.Spacecookie.FileType 6 | 7 | import System.FilePath.Posix.ByteString (takeExtension) 8 | import Test.Tasty 9 | import Test.Tasty.HUnit 10 | 11 | fileTypeDetectionTests :: TestTree 12 | fileTypeDetectionTests = testGroup "spacecookie server file type detection" 13 | [ ioTests 14 | , suffixTests 15 | ] 16 | 17 | ioTests :: TestTree 18 | ioTests = testCase "gopherFileType tests" $ do 19 | assertEqual "Fallback to File without extension" (Right File) 20 | =<< gopherFileType "LICENSE" 21 | 22 | assertEqual "non-existent dot files are forbidden" (Left PathIsNotAllowed) 23 | =<< gopherFileType ".dot-file-missing" 24 | 25 | assertEqual "dot file along the path" (Left PathIsNotAllowed) 26 | =<< gopherFileType ".git/HEAD" 27 | 28 | assertEqual "dot file along the path" (Left PathIsNotAllowed) 29 | =<< gopherFileType "./foo/.git/HEAD" 30 | 31 | assertEqual ".. is disallowed" (Left PathIsNotAllowed) 32 | =<< gopherFileType "/lol/../../doing/directory/traversal.txt" 33 | 34 | assertEqual "\".\" is allowed" (Right Directory) 35 | =<< gopherFileType "." 36 | 37 | assertEqual "txt files" (Right File) 38 | =<< gopherFileType "./docs/rfc1436.txt" 39 | 40 | assertEqual "missing file" (Left PathDoesNotExist) 41 | =<< gopherFileType "missing/this.txt" 42 | 43 | suffixTests :: TestTree 44 | suffixTests = testCase "correct mapping of suffixes" $ do 45 | assertEqual "BinHexMacintoshFile" BinHexMacintoshFile $ 46 | lookupSuffix $ takeExtension "test.hqx" 47 | 48 | assertEqual "tar.gz is BinaryFile" BinaryFile $ 49 | lookupSuffix $ takeExtension "/releases/spacecookie-0.3.0.0.tar.gz" 50 | 51 | assertEqual "gif file" GifFile $ 52 | lookupSuffix $ takeExtension "funny.gif" 53 | 54 | mapM_ (assertEqual "image file" ImageFile . lookupSuffix . takeExtension) 55 | [ "hello.png", "/my/beautiful.jpg", "./../lol.jpeg" 56 | , "../bar.tif", "my.tiff", ".hidden.svg", "my.bmp" ] 57 | 58 | assertEqual "fallback to File" File $ 59 | lookupSuffix $ takeExtension "my/unknown.strange-extension" 60 | -------------------------------------------------------------------------------- /test/Test/Gophermap.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Test.Gophermap (gophermapTests) where 3 | 4 | import Control.Monad (forM_) 5 | import Control.Applicative ((<|>)) 6 | import Data.Attoparsec.ByteString (parseOnly) 7 | import qualified Data.ByteString as B 8 | import Data.Either 9 | import Data.Maybe (fromMaybe) 10 | import Network.Gopher (GopherFileType (..)) 11 | import Network.Gopher.Util.Gophermap 12 | import System.FilePath.Posix.ByteString (RawFilePath) 13 | import Test.Tasty 14 | import Test.Tasty.HUnit 15 | 16 | stripNewline :: B.ByteString -> B.ByteString 17 | stripNewline s = fromMaybe s $ B.stripSuffix "\r\n" s <|> B.stripSuffix "\n" s 18 | 19 | withFileContents :: FilePath -> (IO B.ByteString -> TestTree) -> TestTree 20 | withFileContents path = withResource (B.readFile path) (const (pure ())) 21 | 22 | gophermapTests :: TestTree 23 | gophermapTests = testGroup "gophermap tests" 24 | [ withFileContents "test/data/pygopherd.gophermap" checkPygopherd 25 | , withFileContents "test/data/bucktooth.gophermap" checkBucktooth 26 | , generalGophermapParsing 27 | ] 28 | 29 | checkPygopherd :: IO B.ByteString -> TestTree 30 | checkPygopherd file = testCase "pygopherd example gophermap" $ 31 | file >>= assertEqual "" (Right expectedPygopherd) . parseOnly parseGophermap 32 | 33 | infoLine :: B.ByteString -> GophermapEntry 34 | infoLine b = GophermapEntry InfoLine b Nothing Nothing Nothing 35 | 36 | absDir :: B.ByteString -> RawFilePath -> B.ByteString -> GophermapEntry 37 | absDir n p s = 38 | GophermapEntry Directory n (Just (GophermapAbsolute p)) (Just s) $ Just 70 39 | 40 | expectedPygopherd :: Gophermap 41 | expectedPygopherd = 42 | [ infoLine "Welcome to Pygopherd! You can place your documents" 43 | , infoLine "in /var/gopher for future use. You can remove the gophermap" 44 | , infoLine "file there to get rid of this message, or you can edit it to" 45 | , infoLine "use other things. (You'll need to do at least one of these" 46 | , infoLine "two things in order to get your own data to show up!)" 47 | , infoLine "" 48 | , infoLine "Some links to get you started:" 49 | , infoLine "" 50 | , absDir "Pygopherd Home" "/devel/gopher/pygopherd" "gopher.quux.org" 51 | , absDir "Quux.Org Mega Server" "/" "gopher.quux.org" 52 | , absDir "The Gopher Project" "/Software/Gopher" "gopher.quux.org" 53 | , absDir "Traditional UMN Home Gopher" "/" "gopher.tc.umn.edu" 54 | , infoLine "" 55 | , infoLine "Welcome to the world of Gopher and enjoy!" 56 | ] 57 | 58 | checkBucktooth :: IO B.ByteString -> TestTree 59 | checkBucktooth file = testCase "bucktooth example gophermap" $ do 60 | parseResult <- parseOnly parseGophermap <$> file 61 | 62 | assertBool "no parse failure" $ isRight parseResult 63 | 64 | -- check if we can distinguish between text/infolines and 65 | -- gophermap lines which have no path 66 | assertEqual "overbite link is parsed correctly" [expectedOverbiteEntry] 67 | . filter (\(GophermapEntry _ n _ _ _) -> n == "/overbite") 68 | $ fromRight [] parseResult 69 | 70 | assertEqual "correct length" 95 . length $ fromRight [] parseResult 71 | 72 | expectedOverbiteEntry :: GophermapEntry 73 | expectedOverbiteEntry = 74 | GophermapEntry Directory "/overbite" Nothing Nothing Nothing 75 | 76 | generalGophermapParsing :: TestTree 77 | generalGophermapParsing = testGroup "gophermap entry test cases" $ 78 | let lineEqual :: B.ByteString -> GophermapEntry -> Assertion 79 | lineEqual b e = assertEqual (show b) (Right [e]) $ 80 | parseOnly parseGophermap b 81 | infoLines = 82 | [ "1. beginning with valid file type\n" 83 | , "just some usual text.\n" 84 | , "ends with end of input" 85 | , "i'm blue" 86 | , "0" 87 | , "empty ones need to be terminated by a new line\n" 88 | , "\n" 89 | , "otherwise parsing doesn't make sense anymore" 90 | , "DOS-style newlines are also allowed\r\n" 91 | ] 92 | menuEntry t name path = 93 | GophermapEntry t name (Just path) Nothing Nothing 94 | menuLines = 95 | [ ("1/somedir\t", GophermapEntry Directory "/somedir" Nothing Nothing Nothing) 96 | , ("0file\tfile.txt\n", menuEntry File "file" (GophermapRelative "file.txt")) 97 | , ("ggif\t/pic.gif", menuEntry GifFile "gif" (GophermapAbsolute "/pic.gif")) 98 | , ("hcode\tURL:https://code.sterni.lv\n", menuEntry Html "code" (GophermapUrl "URL:https://code.sterni.lv")) 99 | , ("1foo\tfoo\tsterni.lv", GophermapEntry Directory "foo" (Just $ GophermapRelative "foo") (Just "sterni.lv") Nothing) 100 | , ("Ibar\t/bar.png\tsterni.lv\t7070\n", GophermapEntry ImageFile "bar" (Just $ GophermapAbsolute "/bar.png") (Just "sterni.lv") (Just 7070)) 101 | , ("imanual info line\t", infoLine "manual info line") 102 | ] 103 | in [ testCase "info lines" $ forM_ infoLines (\l -> lineEqual l $ infoLine (stripNewline l)) 104 | , testCase "menu entries" $ forM_ menuLines (uncurry lineEqual) ] 105 | -------------------------------------------------------------------------------- /test/Test/Integration.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Test.Integration where 3 | 4 | import Control.Applicative ((<|>)) 5 | import Control.Concurrent (threadDelay) 6 | import Control.Exception (bracket) 7 | import Control.Monad (forM_) 8 | import Data.ByteString (ByteString) 9 | import qualified Data.ByteString as B 10 | import Data.Char (ord) 11 | import Data.List 12 | import Data.Maybe (isNothing, isJust, fromJust) 13 | import Network.Curl.Download (openURI) 14 | import System.Directory (findExecutable) 15 | import System.Environment (lookupEnv) 16 | import System.Exit (ExitCode (..)) 17 | import System.Process (spawnProcess, terminateProcess, waitForProcess) 18 | import Test.Tasty 19 | import Test.Tasty.ExpectedFailure 20 | import Test.Tasty.HUnit 21 | import Test.Tasty.Providers (testPassed) 22 | import Test.Tasty.Runners (Result (..)) 23 | 24 | spacecookieBin :: IO (Maybe FilePath) 25 | spacecookieBin = do 26 | fromEnv <- lookupEnv "SPACECOOKIE_TEST_BIN" 27 | fromPath <- findExecutable "spacecookie" 28 | pure $ fromEnv <|> fromPath 29 | 30 | ignoreTestIf :: IO Bool -> String -> TestTree -> TestTree 31 | ignoreTestIf doSkip msg tree = wrapTest change tree 32 | where change normal = do 33 | skip <- doSkip 34 | if not skip 35 | then normal 36 | else pure $ (testPassed msg) { 37 | resultShortDescription = "SKIP" 38 | } 39 | 40 | integrationTests :: TestTree 41 | integrationTests = testGroup "integration tests" 42 | [ ignoreTestIf (isNothing <$> spacecookieBin) "no spacecookie executable" 43 | $ testCaseSteps "spacecookie server behaves as expected" integrationAsserts 44 | ] 45 | 46 | integrationAsserts :: (String -> IO ()) -> Assertion 47 | integrationAsserts step = do 48 | step "getting spacecookie executable" 49 | bin <- spacecookieBin 50 | assertBool "have spacecookie executable" $ isJust bin 51 | step "starting spacecookie executable" 52 | bracket (spawn (fromJust bin)) assertSuccess 53 | $ const $ do 54 | threadDelay 1000000 -- wait 1 sec for the server to start up 55 | step "request root menu" 56 | 57 | assertEqual "root menu as expected" (Right expectedRoot) 58 | =<< openURI "gopher://localhost:7000/0" 59 | 60 | assertEqual "root menu requested with / as expected" (Right expectedRoot) 61 | =<< openURI "gopher://localhost:7000/0/" 62 | 63 | step "request plain.txt" 64 | 65 | fileDisk <- Right <$> B.readFile "test/integration/root/plain.txt" 66 | fileGopher <- openURI "gopher://localhost:7000/1/plain.txt" 67 | 68 | assertEqual "served file is same as on disk" fileDisk fileGopher 69 | 70 | step "check automatically generated directory menus" 71 | 72 | dir <- openURI "gopher://localhost:7000/0/dir" 73 | dirNoSlash <- openURI "gopher://localhost:7000/0dir" 74 | 75 | assertEqual "directory menu is equal regardless of request" dir dirNoSlash 76 | 77 | -- ignore ordering for the purpose of this test 78 | assertEqual "directory menu contains expected entries" (Right expectedDir) 79 | $ sort . filter (not . B.null) . B.split (fromIntegral $ ord '\n') <$> dir 80 | 81 | step "sanity check not found error messages" 82 | 83 | notFoundError <- openURI "gopher://localhost:7000/1/does/not/exist" 84 | anotherNotFoundError <- openURI "gopher://localhost:7000/0/not-here.txt" 85 | urlNotFoundError <- openURI "gopher://localhost:7000/0URL:http://sterni.lv" 86 | 87 | assertEqual "precise error message" 88 | (Right expectedErrorMessage) anotherNotFoundError 89 | 90 | forM_ [ notFoundError, anotherNotFoundError, urlNotFoundError ] 91 | $ assertIsError 92 | 93 | assertBool "error responses differ for different files" 94 | $ notFoundError /= anotherNotFoundError 95 | 96 | assertBool "error response for URL: selectors is helpful" 97 | $ Right True == fmap (B.isInfixOf "support") urlNotFoundError 98 | 99 | step "sanity check not allowed error messages" 100 | 101 | -- can't test directory traversal since curl won't try it 102 | accessGophermap <- openURI "gopher://localhost:7000/0/.gophermap" 103 | accessNonExistentDot <- openURI "gopher://localhost:7000/0/dir/.not-here" 104 | 105 | forM_ [ accessGophermap, accessNonExistentDot ] $ \err -> do 106 | assertIsError err 107 | assertBool "error response is not allowed response" 108 | $ Right True == fmap (B.isInfixOf "allow") err 109 | 110 | where spawn bin = spawnProcess bin [ "test/integration/spacecookie.json" ] 111 | assertSuccess hdl = do 112 | step "stopping spacecookie" 113 | terminateProcess hdl 114 | assertEqual "spacecookie's exit code indicates SIGTERM" (ExitFailure (-15)) 115 | =<< waitForProcess hdl 116 | assertIsError e = assertEqual "error response starts with a 3" (Right "3") 117 | $ fmap (B.take 1) e 118 | 119 | expectedRoot :: ByteString 120 | expectedRoot = mconcat 121 | [ "iHello World!\tHello World!\tlocalhost\t7000\r\n" 122 | , "i\t\tlocalhost\t7000\r\n" 123 | , "0normal text file\t/plain.txt\tlocalhost\t7000\r\n" 124 | , "1normal dir\t/dir\tlocalhost\t7000\r\n" 125 | , "i\t\tlocalhost\t7000\r\n" 126 | , "1external 1\t/\tthis.is.bogus.org\t7000\r\n" 127 | , "1external 2\t/\tsdf.org\t70\r\n" 128 | ] 129 | 130 | expectedDir :: [ByteString] 131 | expectedDir = sort 132 | [ "1another\t/dir/another\tlocalhost\t7000\r" 133 | , "0mystery-file\t/dir/mystery-file\tlocalhost\t7000\r" 134 | , "0strange.tXT\t/dir/strange.tXT\tlocalhost\t7000\r" 135 | , "4macintosh.hqx\t/dir/macintosh.hqx\tlocalhost\t7000\r" 136 | ] 137 | 138 | expectedErrorMessage :: ByteString 139 | expectedErrorMessage = mconcat 140 | [ "3The requested resource '/not-here.txt' does not exist" 141 | , " or is not available.\tErr\tlocalhost\t7000\r\n" ] 142 | -------------------------------------------------------------------------------- /test/Test/Sanitization.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Test.Sanitization (sanitizationTests) where 3 | 4 | import Network.Spacecookie.FileType (checkNoDotFiles, PathError (..)) 5 | import Network.Spacecookie.Path (sanitizePath, makeAbsolute) 6 | 7 | import Control.Monad (forM_) 8 | import qualified Data.ByteString.UTF8 as UTF8 9 | import System.FilePath.Posix.ByteString (isAbsolute) 10 | import Test.Tasty 11 | import Test.Tasty.HUnit 12 | 13 | sanitizationTests :: TestTree 14 | sanitizationTests = testGroup "Sanitization of user input" 15 | [ pathSanitization 16 | , dotFileDetectionTest 17 | , makeAbsoluteTest 18 | ] 19 | 20 | pathSanitization :: TestTree 21 | pathSanitization = testCase "sanitizePath behavior" $ do 22 | let assertSanitize e p = assertEqual p e $ sanitizePath (UTF8.fromString p) 23 | assertSanitize "/root" "/root" 24 | assertSanitize "/home/alice/.emacs.d/init.el" "/home/alice/.emacs.d/init.el" 25 | 26 | assertSanitize "root" "./root" 27 | assertSanitize"/tools/magrathea" "//tools/magrathea" 28 | assertSanitize "/home/bob/Documents/important.txt" "/home/bob//Documents/important.txt" 29 | 30 | assertSanitize "foo/bar/baz.txt" "./foo/bar/./baz.txt" 31 | assertSanitize "/var/www/index..html" "/var/www/.///index..html" 32 | assertSanitize "./" "./." 33 | assertSanitize "/" "/." 34 | assertSanitize "home/eve/" "./home/./././eve////./." 35 | 36 | assertSanitize "/home/bob/alice/private.txt" "/home/bob/../alice/private.txt" 37 | 38 | dotFileDetectionTest :: TestTree 39 | dotFileDetectionTest = testCase "spacecookie server detects dot files in paths" $ do 40 | let assertDot p hasDot = forM_ 41 | [ (p, UTF8.fromString p) 42 | , (p ++ " (sanitized)", sanitizePath (UTF8.fromString p)) 43 | ] 44 | $ \(title, path) -> assertEqual title 45 | (if hasDot then Left PathIsNotAllowed else Right ()) 46 | $ checkNoDotFiles path 47 | 48 | assertDot "./normal/relative/path" False 49 | assertDot "." False 50 | assertDot "/some/absolute/path" False 51 | assertDot "file.txt" False 52 | assertDot "/foo.html" False 53 | assertDot "./tmp/scratch.txt" False 54 | assertDot "./." False 55 | assertDot "relative/./path" False 56 | 57 | assertDot ".emacs.d/init.el" True 58 | assertDot ".gophermap" True 59 | assertDot "/home/bob/.vimrc" True 60 | assertDot "/home/alice/.config/foot" True 61 | assertDot "./nixpkgs/.git/config" True 62 | 63 | -- only fail prior to sanitization 64 | forM_ 65 | [ "dir/../traversal/../attack", "../../../actual/traversal" ] 66 | $ \p -> do 67 | let p' = UTF8.fromString p 68 | assertEqual p (Left PathIsNotAllowed) $ checkNoDotFiles p' 69 | assertEqual p (Right ()) $ checkNoDotFiles (sanitizePath p') 70 | 71 | makeAbsoluteTest :: TestTree 72 | makeAbsoluteTest = testCase "relative paths are correctly converted to absolute ones" $ do 73 | let assertAbsolute expected given = do 74 | assertEqual given expected $ makeAbsolute (UTF8.fromString given) 75 | assertBool ("makeAbsolute " ++ given ++ " is absolute") $ isAbsolute (makeAbsolute (UTF8.fromString given)) 76 | 77 | assertAbsolute "/foo/bar" "/foo/bar" 78 | assertAbsolute "/foo/bar" "./foo/bar" 79 | assertAbsolute "/foo/bar" "foo/bar" 80 | assertAbsolute "/" "." 81 | assertAbsolute "/" "./" 82 | assertAbsolute "/bar/foo" "././bar/foo" 83 | assertAbsolute "/../bar/foo" "./../bar/foo" 84 | assertAbsolute "/../bar/foo" "../bar/foo" 85 | -------------------------------------------------------------------------------- /test/data/bucktooth.gophermap: -------------------------------------------------------------------------------- 1 | Welcome to Floodgap Systems' official gopher server. 2 | Floodgap has served the gopher community since 1999 3 | (formerly gopher.ptloma.edu). ** OVER A DECADE OF SERVICE! ** 4 | 5 | We run Bucktooth 0.2.8 on xinetd as our server system. 6 | gopher.floodgap.com is an IBM Power 520 Express with a 2-way 7 | 4.2GHz POWER6 CPU and 8GB of RAM, running AIX 6.1 TL6. 8 | Send gopher@floodgap.com your questions and suggestions. 9 | 10 | THIS IS A NEW SERVER -- bugs are to be expected. 11 | E-mail weird behaviour to us. 4/2011 12 | 13 | *********************************************************** 14 | ** CELEBRATING GOPHER'S 20 YEAR ANNIVERSARY! ** 15 | ** Plain text is beautiful! ** 16 | *********************************************************** 17 | 18 | 0Does this gopher menu look correct? /gopher/proxy 19 | (plus using the Floodgap Public Gopher Proxy) 20 | 1Super-Dimensional Fortress: SDF Gopherspace sdf.org 70 21 | Get your own Gopherspace and shell account! 22 | 23 | --- Getting started with Gopher ----------------------------------- 24 | 1Getting started with gopher, software, more /gopher 25 | (what is Gopherspace? We tell you! And find out how 26 | to create your own Gopher world!) 27 | 28 | 0Using web browsers in Gopherspace /gopher/wbgopher 29 | (READ IT! LEARN IT! LOVE IT!) 30 | (useful tips for gopher newbies, updated 11 November 2010) 31 | 32 | 1/overbite 33 | (download gopher add-ons for Mozilla Firefox, Google Chrome, 34 | mobile clients for Android and more! Put Gopherspace on 35 | your mobile phone or desktop computer!) 36 | 1Other Gopher clients for various platforms /gopher/clients 37 | 38 | --- Find and search for other Gopher sites on the Internet -------- 39 | 1Search Gopherspace with Veronica-2 and VISHNU /v2 40 | or search all known titles in Gopherspace with Veronica-2 here: 41 | 7Search Veronica-2 /v2/vs 42 | 43 | 1All the gopher servers in the world (that we know of) /world 44 | (updated with robot updates) 45 | 1New Gopher servers since 1999 /new 46 | (updated 25 April 2011) 47 | 48 | --- Get news, weather and more through Gopherspace ---------------- 49 | 1Weather maps and forecasts via Floodgap Groundhog /groundhog 50 | (updates occur throughout the day) 51 | 1News and headline feeds via Flood Feeds /feeds 52 | (updates occur daily/regularly) 53 | 1Most current Floodgap news feeds /feeds/latest 54 | (today's most updated news and headlines) 55 | 0United States Geological Survey earthquake list /quakes 56 | (up to date USGS earthquake information for all states w/AK, HI, PR 57 | and selected international locations) 58 | (updated on access) 59 | 0Caltrans California highway conditions /calroads 60 | (hourly updates on California highway conditions) 61 | (updated on access) 62 | 63 | --- File archives and downloads ----------------------------------- 64 | 1Floodgap File Archives and Mirrors /archive 65 | (includes external archives and historical files, 66 | Walnut Creek CP/M-Osborne-Commodore-Beehive archives, 67 | Info-Mac, classic Mac software and more) 68 | 69 | --- Fun, games, and other neat things ----------------------------- 70 | 1Floodgap Gopher Fun and Games /fun 71 | (with xkcd, Hitori Dake no Renga, the Gopher Figlet gateway 72 | and Twitpher, the Twitter->gopher interface) 73 | 1Floodgap users and staff gopher pages /users 74 | (the usual gang of idiots) 75 | 1The New GopherVR: A Virtual Reality View of Gopherspace /gophervr 76 | (version 0.4.1 released 10 September 2010) 77 | 78 | --- Server software behind the scenes ----------------------------- 79 | 1The Bucktooth gopher server /buck 80 | (version 0.2.8 released 23 June 2010) 81 | 82 | --- Gopherspace advocacy and activism ----------------------------- 83 | 1Floodgap Gopher Statistics Project /gstats 84 | (monthly traffic analysis of the Floodgap Public Gopher Proxy 85 | for community advocacy purposes; updates monthly) 86 | 87 | 1The Floodgap Free Software License /ffsl 88 | 0Where's Floodgap? (not for hatemail ;-) /whereis 89 | 0"/usr/bin/tail" our gopher server log /recent 90 | 0RIP, Master gopher at University of Minnesota umngone 91 | hFloodgap.com (Web pages) URL:http://www.floodgap.com/ 92 | 93 | Please note that this gopher is now an independent 94 | entity and is no longer affiliated with Point Loma 95 | Nazarene University. 96 | -------------------------------------------------------------------------------- /test/data/pygopherd.gophermap: -------------------------------------------------------------------------------- 1 | Welcome to Pygopherd! You can place your documents 2 | in /var/gopher for future use. You can remove the gophermap 3 | file there to get rid of this message, or you can edit it to 4 | use other things. (You'll need to do at least one of these 5 | two things in order to get your own data to show up!) 6 | 7 | Some links to get you started: 8 | 9 | 1Pygopherd Home /devel/gopher/pygopherd gopher.quux.org 70 10 | 1Quux.Org Mega Server / gopher.quux.org 70 11 | 1The Gopher Project /Software/Gopher gopher.quux.org 70 12 | 1Traditional UMN Home Gopher / gopher.tc.umn.edu 70 13 | 14 | Welcome to the world of Gopher and enjoy! 15 | -------------------------------------------------------------------------------- /test/integration/root/.gophermap: -------------------------------------------------------------------------------- 1 | Hello World! 2 | 3 | 0normal text file plain.txt 4 | 1normal dir dir 5 | 6 | 1external 1 / this.is.bogus.org 7 | 1external 2 / sdf.org 70 8 | -------------------------------------------------------------------------------- /test/integration/root/dir/.hidden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sternenseemann/spacecookie/b6a9d7021d9fba82b1055f81a9890e961efa0530/test/integration/root/dir/.hidden -------------------------------------------------------------------------------- /test/integration/root/dir/another/.git-hello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sternenseemann/spacecookie/b6a9d7021d9fba82b1055f81a9890e961efa0530/test/integration/root/dir/another/.git-hello -------------------------------------------------------------------------------- /test/integration/root/dir/macintosh.hqx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sternenseemann/spacecookie/b6a9d7021d9fba82b1055f81a9890e961efa0530/test/integration/root/dir/macintosh.hqx -------------------------------------------------------------------------------- /test/integration/root/dir/mystery-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sternenseemann/spacecookie/b6a9d7021d9fba82b1055f81a9890e961efa0530/test/integration/root/dir/mystery-file -------------------------------------------------------------------------------- /test/integration/root/dir/strange.tXT: -------------------------------------------------------------------------------- 1 | Lorenz Westenrieder 2 | -------------------------------------------------------------------------------- /test/integration/root/plain.txt: -------------------------------------------------------------------------------- 1 | Es ist eine natürliche Vorstellung, daß, ehe in der Philosophie an die Sache selbst, nämlich 2 | an das wirkliche Erkennen dessen, was in Wahrheit ist, gegangen wird, es notwendig sei, vorher 3 | über das Erkennen sich zu verständigen, das als das Werkzeug, wodurch man des Absoluten 4 | sich bemächtige, oder als das Mittel, durch welches hindurch man es erblicke, betrachtet 5 | wird. Die Besorgnis scheint gerecht, teils, daß es verschiedene Arten der Erkenntnis geben 6 | und darunter eine geschickter als eine andere zur Erreichung dieses Endzwecks sein möchte, 7 | hiermit auch falsche Wahl unter ihnen, – teils auch daß, indem das Erkennen ein Vermögen 8 | von bestimmter Art und Umfange ist, ohne die genauere Bestimmung seiner Natur und Grenze 9 | Wolken des Irrtums statt des Himmels der Wahrheit erfaßt werden. Diese Besorgnis muß sich 10 | wohl sogar in die Überzeugung verwandeln, daß das ganze Beginnen, dasjenige, was an sich 11 | ist, durch das Erkennen dem Bewußtsein zu erwerben, in seinem Begriffe widersinnig sei, und 12 | zwischen das Erkennen und das Absolute eine sie schlechthin scheidende Grenze falle. Denn ist 13 | das Erkennen das Werkzeug, sich des absoluten Wesens zu bemächtigen, so fällt sogleich auf, 14 | daß die Anwendung eines Werkzeugs auf eine Sache sie vielmehr nicht läßt, wie sie für sich 15 | ist, sondern eine Formierung und Veränderung mit ihr vornimmt. Oder ist das Erkennen nicht 16 | Werkzeug unserer Tätigkeit, sondern gewissermaßen ein passives Medium, durch welches hindurch 17 | das Licht der Wahrheit an uns gelangt, so erhalten wir auch so sie nicht, wie sie an sich, 18 | sondern wie sie durch und in diesem Medium ist. Wir gebrauchen in beiden Fällen ein Mittel, 19 | welches unmittelbar das Gegenteil seines Zwecks hervorbringt; oder das Widersinnige ist vielmehr, 20 | daß wir uns überhaupt eines Mittels bedienen. Es scheint zwar, daß diesem Übelstande durch die 21 | Kenntnis der Wirkungsweise des Werkzeugs abzuhelfen steht, denn sie macht es möglich, den Teil, 22 | welcher in der Vorstellung, die wir durch es vom Absoluten erhalten, dem Werkzeuge angehört, im 23 | Resultate abzuziehen und so das Wahre rein zu erhalten. Allein diese Verbesserung würde uns in 24 | der Tat nur dahin zurückbringen, wo wir vorher waren. Wenn wir von einem formierten Dinge das 25 | wieder wegnehmen, was das Werkzeug daran getan hat, so ist uns das Ding – hier das Absolute 26 | – gerade wieder soviel als vor dieser somit überflüssigen Bemühung. Sollte das Absolute 27 | durch das Werkzeug uns nur überhaupt nähergebracht werden, ohne etwas an ihm zu verändern, 28 | wie etwa durch die Leimrute der Vogel, so würde es wohl, wenn es nicht an und für sich schon 29 | bei uns wäre und sein wollte, dieser List spotten; denn eine List wäre in diesem Falle das 30 | Erkennen, da es durch sein vielfaches Bemühen ganz etwas anderes zu treiben sich die Miene gibt, 31 | als nur die unmittelbare und somit mühelose Beziehung hervorzubringen. Oder wenn die Prüfung 32 | des Erkennens, das wir als ein Medium uns vorstellen, uns das Gesetz seiner Strahlenbrechung 33 | kennen lehrt, so nützt es ebenso nichts, sie im Resultate abzuziehen; denn nicht das Brechen 34 | des Strahls, sondern der Strahl selbst, wodurch die Wahrheit uns berührt, ist das Erkennen, 35 | und dieses abgezogen, wäre uns nur die reine Richtung oder der leere Ort bezeichnet worden. 36 | -------------------------------------------------------------------------------- /test/integration/spacecookie.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostname" : "localhost", 3 | "listen" : { 4 | "addr" : "::", 5 | "port" : 7000 6 | }, 7 | "user" : null, 8 | "root" : "./test/integration/root", 9 | "log" : { 10 | "enable" : false 11 | } 12 | } 13 | --------------------------------------------------------------------------------