├── .gitignore ├── assets ├── file.html └── directory.html ├── host.nimble ├── man └── man1 │ └── host.1 ├── README.md └── src └── host.nim /.gitignore: -------------------------------------------------------------------------------- 1 | host 2 | output -------------------------------------------------------------------------------- /assets/file.html: -------------------------------------------------------------------------------- 1 | {file_name} 2 | -------------------------------------------------------------------------------- /host.nimble: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Package 4 | 5 | version = "1.2.1" 6 | author = "Rainbow Asteroids" 7 | description = "A program to staticlly host files or directories over HTTP" 8 | license = "GPL-3.0" 9 | srcDir = "src" 10 | bin = @["host"] 11 | 12 | 13 | # Dependencies 14 | 15 | requires "nim >= 1.4.2" 16 | 17 | # Tasks 18 | 19 | task release, "Builds the release version of host and puts it a output/ directory": 20 | mkDir("output") 21 | exec("nim c -d:release --outdir:output src/host") 22 | 23 | after install: 24 | if system.hostOS == "linux": 25 | echo "Adding man pages to ~/.nimble/man..." 26 | 27 | if not dirExists($getHomeDir() & "/.nimble/man"): 28 | cpDir("man", $getHomeDir() & "/.nimble/man") 29 | echo "Man page installed!" 30 | 31 | if not staticExec("manpath").contains(".nimble/man"): 32 | echo "~/.nimble/man isn't in your manpath! Be sure fix that for access to the host man page." 33 | -------------------------------------------------------------------------------- /assets/directory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | host - {warn} 4 | 72 | 73 | 74 |
75 | host - {warn} 76 |
77 |
78 |

{path}

79 |
80 | {files} 81 |
82 |
83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /man/man1/host.1: -------------------------------------------------------------------------------- 1 | .TH host 1 "January 28, 2021" "Version 1.2.1" 2 | .SH NAME 3 | host - a simple, static, web server for easily hosting files over LAN 4 | 5 | .SH SYNOPSIS 6 | host --help 7 | 8 | host [-h] [-q] FILE 9 | 10 | host [--hidden] [--quiet] [--output OUTPUT_FILE] [--port PORT] FILE 11 | 12 | host -i [-h] [-q] 13 | 14 | host --stdin [--hidden] [--quiet] [--output OUTPUT_FILE] [--port PORT] 15 | 16 | .SH DESCRIPTION 17 | 18 | host is a simple, static, web server for easily hosting files over LAN. host 19 | makes no security guarantees, so it should only be used on a LAN that you trust 20 | (no port forwarding and probably no coffee shops). 21 | 22 | host runs in three different modes: file, stdin, and directory. 23 | 24 | .PP 25 | In file mode, the server responds to GET requests with the contents of the 26 | file, regardless of the route sent. In file mode, the program reads the 27 | contents of the file every time a GET request is sent. 28 | 29 | Stdin mode acts similarly to file mode, just that host reads from stdin instead 30 | of a file and stdin is only read once. Because of this read-once property of 31 | host, it could be used as a method to host a file without changes on disk 32 | affecting changes in what is being hosted. 33 | 34 | .PP 35 | Directory mode is very different. In directory mode, the server acts like a 36 | real server, allowing the clients to traverse the directory tree, where the 37 | root of the directory tree is the specified directory (see FILE above). host 38 | has an html template, known as directory.html, embeded inside. This html 39 | template is invoked every time a user sends a request to a directory that does 40 | not contain an index.html file, when the user adds the ls key to their query 41 | (localhost:PORT/foobar?ls), or when the user requests a file that does not 42 | exist. 43 | 44 | .PP 45 | These modes are selected at runtime based on the command line arguments passed 46 | into host. If the -i or --stdin option is selected, host will execute in stdin 47 | mode. If a file is passed into the FILE argument (see SYNOPSIS), host will run 48 | in file mode. Likewise, if a directory is passed into the FILE argument, host 49 | will run in directory mode. 50 | 51 | .PP 52 | The way host handles mimetypes depends on the mode host is in. If host is in 53 | directory or file mode, host will respond with the mimetype appropriate for 54 | the file, based on the file extension, with the default being text/plain. 55 | If host is started in stdin mode, host will respond with the text/plain 56 | mimetype. 57 | 58 | Using the --mime option, and if host is in file or stdin mode, host will 59 | start with the mimetype described. Note: the --mime option expects file 60 | extentions, so use html instead of text/html, txt instead of 61 | text/plain, etc 62 | 63 | .SH OPTIONS 64 | 65 | .TP 66 | .B --help 67 | 68 | Displays help text and exits 69 | 70 | .TP 71 | .B -h, --hidden 72 | 73 | Shows hidden files. This option is only useful in directory mode and will do 74 | nothing if it is outside of directory mode. 75 | 76 | .TP 77 | .B -i, --stdin 78 | 79 | Puts host into stdin mode and will serve whatever it finds from stdin. 80 | 81 | .TP 82 | .B --mime [mimetype] 83 | 84 | Sets the mimetype that will be sent by host. This is useful for stdin mode, as 85 | the default mimetype is text/plain. In file mode, the mimetype is determined 86 | via the file extension. This option overrides that. Mimetypes should be 87 | inputted via their file extensions and not the `type/standard` format. 88 | (so, html over text/html, txt over text/plain, etc.) 89 | 90 | .TP 91 | .B --output [FILE] 92 | 93 | Instead of printing out request logs, the request logs will be silently sent 94 | to FILE. 95 | 96 | .TP 97 | .B --port [PORT] 98 | 99 | Set the port host will be host on. Set to 8080 by default. 100 | 101 | .TP 102 | .B -q, --quiet 103 | 104 | Prevents host from printing request logs. 105 | 106 | .SH AUTHOR 107 | 108 | Please send all bug reports to the GitHub issue tracker: 109 | .B https://github.com/RainbowAsteroids/host/issues 110 | 111 | host was written by Rainbow Asteroids (rainbowasteroids@protonmail.com) in Nim. 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # host 2 | host is a simple, static, web server for easily hosting files over LAN. 3 | host makes no security guarantees, so it should only be used on a LAN that you 4 | trust (no port forwarding and probably no coffee shops). 5 | 6 | ## why host? 7 | host offers files over http instead of ftp or ssh, making files much more 8 | accessible to any device with a browser (which is every device). your browser 9 | can also render html, svgs, pictures, videos, pdfs, etc. without the need of 10 | additional software. of cource, host is read-only, so if you need your clients 11 | to write to your server too, it's better to use real file sharing 12 | software/protocols. any linux box should have built-in support for sftp. 13 | 14 | ## how do i get host? 15 | host is written in [Nim](https://nim-lang.org) and uses pure Nim standard 16 | libraries, meaning the only thing you need to build host is the Nim compiler. 17 | to build and install the project: 18 | 19 | 1. install Nim ~~(try your package manager, `sudo pacman -Syu nim` or 20 | `sudo apt install nim`)~~ after some 21 | [helpful people](https://old.reddit.com/r/nim/comments/l74l95/host_is_a_simple_static_web_server_for_lan/gl4rgbk/) 22 | told me that package managers have a tendency of holding outdated versions of 23 | Nim, it is now recommended by this README to get Nim from 24 | [choosenim](https://github.com/dom96/choosenim). 25 | 26 | 2. install host via running `nimble install host` 27 | 28 | to uninstall, use `nimble uninstall host` 29 | 30 | # using host 31 | 32 | ## quick examples 33 | 34 | host directory we are in 35 | ``` 36 | host . 37 | ``` 38 | 39 | host it on port 9001 40 | 41 | ``` 42 | host --port 9001 . 43 | ``` 44 | 45 | host a cool file 46 | ``` 47 | host my-cool-website.html 48 | ``` 49 | 50 | host something from stdin 51 | ``` 52 | find | host 53 | ``` 54 | 55 | learn more about host 56 | ``` 57 | host --help 58 | man host # Linux only! 59 | ``` 60 | 61 | ## usage 62 | 63 | ``` 64 | host --help 65 | ``` 66 | 67 | ### file/directory mode 68 | 69 | 70 | ``` 71 | host [-h] [-q] FILE 72 | ``` 73 | 74 | ``` 75 | host [--hidden] [--quiet] [--output OUTPUT_FILE] [--port PORT] FILE 76 | ``` 77 | 78 | ### stdin mode 79 | 80 | ``` 81 | host -i [-h] [-q] 82 | ``` 83 | 84 | ``` 85 | host --stdin [--hidden] [--quiet] [--output OUTPUT_FILE] [--port PORT] 86 | ``` 87 | 88 | ## description 89 | 90 | once host starts, you'll see an IP address and a port. that ip and port combo 91 | need to be used to access host on other devices. take out a phone (or other 92 | device on the same wifi network) and type into the url bar `IP_ADDRESS:PORT`. 93 | if you want to access host from the same machine that is running host, simply 94 | point your web browser of choice to `localhost:PORT`. 95 | 96 | ### modes 97 | 98 | host runs in three different modes: file, directory, and stdin 99 | 100 | in file mode, host will respond to all requests with the file inputted as the 101 | `FILE` parameter, regardless of the path, body, or headers sent. every time 102 | host handles a request in file mode, host will read the file first, then send 103 | it. if this isn't behavior you want, you can use stdin mode 104 | (`cat FILE | host`) 105 | 106 | in stdin mode, host will do the same thing as if it was in stdin mode, however, 107 | host will send back the data it got from standard in, instead of the file from 108 | the `FILE` parameter. 109 | 110 | in directory mode, host will act like a real server. host will read the path 111 | of any request and act accordingly. if the path points to a file, then host 112 | will send the file back. if the path is a directory, host searches for a 113 | `index.html` in that directory. if host does not find `index.html`, host 114 | will send back a view of all the file and folders in the directory 115 | (otherwise known as `directory.html`). if the `ls` query is sent (so host sees 116 | IP:PORT/path/to/foo/bar/?ls), host will automatically send the directory view 117 | without checking if the directory has a `index.html` if the path points to 118 | nothing (in other words, a 404), host will send back a directory view of the 119 | parent directory (so `IP:PORT/path/to/nothing/404` will return the directory 120 | view of `path/to/nothing`). 121 | 122 | these modes are selected based on the settings host is launched with. if there 123 | is a stdin option (`host -i ...`, `host --stdin ...`), host will start in stdin 124 | mode. if host is given a file for the `FILE` parameter, host will start in file 125 | mode. if host is given a directory for the `FILE` parameter, host will start in 126 | directory mode. 127 | 128 | ### mimetypes 129 | 130 | the way host handles mimetypes depends on the mode host is in. if host is in 131 | directory or file mode, host will respond with the mimetype appropriate for 132 | the file, based on the file extension, with the default being `text/plain`. 133 | if host is started in stdin mode, host will respond with the `text/plain` 134 | mimetype. 135 | 136 | using the `--mime` option, and if host is in file or stdin mode, host will 137 | start with the mimetype described. *note: the `--mime` option expects file 138 | extentions, so use `html` instead of `text/html`, `txt` instead of 139 | `text/plain`, etc* 140 | 141 | ## options 142 | 143 | **--help** 144 | 145 | shows host's help text 146 | 147 | **-h**, **--hidden** 148 | 149 | shows hidden files (the files that start with `.`). this is only useful in 150 | directory mode. 151 | 152 | **-i**, **--stdin** 153 | 154 | starts host in stdin mode. read the description/modes section for more info 155 | 156 | **--mime [mimetype]** 157 | 158 | sets the mimetype host will send. `[mimetype]` must be a file extension, not 159 | a normal mimetype. only useful in file and stdin modes. read 160 | description/mimetypes for more info 161 | 162 | **--output [file]** 163 | 164 | sets the file host will log requests to. 165 | 166 | **--port [port]** 167 | 168 | sets the port host will run on 169 | 170 | **-q**, **--quiet** 171 | 172 | prevents logging 173 | -------------------------------------------------------------------------------- /src/host.nim: -------------------------------------------------------------------------------- 1 | import asynchttpserver, asyncdispatch 2 | import strutils, os, re, algorithm, tables 3 | import mimetypes, strformat, parseopt, parseutils 4 | import net 5 | 6 | # Embed some important resources 7 | const directory_html = slurp"../assets/directory.html" 8 | const file_html = slurp"../assets/file.html" 9 | const help_text = """Usage: host [options] [file] 10 | 11 | Options: 12 | --help : Show this help text 13 | -h, --hidden : Shows hidden files. Only useful in directory mode. 14 | -i, --stdin : Host from stdin. Ignores file parameter. 15 | --mime [mimetype] : Set the mimetype of the file/stdin. Ignored in directory mode. 16 | --output [file] : Output request logs to a file. 17 | --port [port] : Host on a specific port (default 8080) 18 | -q, --quiet : Hide request logs.""" 19 | 20 | type 21 | InputType = enum 22 | itFile, 23 | itDir, 24 | itStdIn 25 | 26 | Input = object 27 | case inputType: InputType 28 | of itFile, itDir: path: string 29 | of itStdIn: str: string 30 | 31 | type 32 | RequestLogType = enum 33 | rltEcho, rltNone, rltFile 34 | 35 | RequestLogState = object 36 | case rlType: RequestLogType 37 | of rltFile: file: File 38 | else: discard 39 | 40 | var showHidden = false 41 | var requestLogState = RequestLogState(rlType: rltEcho) 42 | 43 | proc combineDir(sub, tail: string): string = 44 | let tail = if tail[0] == '/': tail else: '/' & tail 45 | if sub[^1] == '/': sub[0..^2] & tail 46 | else: sub & tail 47 | 48 | proc logRequest(req: Request) = 49 | if requestLogState.rlType != rltNone: 50 | let log = fmt"req: {req.url.path} ; by: {req.hostname}" 51 | if requestLogState.rlType == rltFile: 52 | requestLogState.file.write(log & '\n') 53 | else: 54 | echo log 55 | 56 | proc generateHeader(mime: string): HttpHeaders = 57 | [("content-type", fmt"{mime}; charset=UTF-8")].newHttpHeaders 58 | 59 | proc generateRequestHandlerFromString(data, mime: string): proc = 60 | let header = generateHeader(mime) 61 | proc requestHandler(req: Request) {.async.} = 62 | req.logRequest() 63 | await req.respond(Http200, data, header) 64 | requestHandler 65 | 66 | proc generateRequestHandlerFromFile(fileName, mime: string): proc = 67 | let header = generateHeader(mime) 68 | proc requestHandler(req: Request) {.async.} = 69 | req.logRequest() 70 | await req.respond(Http200, readFile(fileName), header) 71 | requestHandler 72 | 73 | proc generateRequestHandlerFromDirectory(dir: string): proc = 74 | proc requestHandler(req: Request) {.async.} = 75 | # 0. Check that the user isn't requesting outside of our dir 76 | # 1. Check if we should invoke directory.html 77 | # 2. Check if requested file is a directory 78 | # 2a. No, send file 79 | # 3. If it is a directory, look for index.html 80 | # 3a. If found, send index.html 81 | # 3b. If not found, send directory.html 82 | # 4. If nothing is found, send a 404 83 | 84 | type Warning = enum 85 | wNoIndex = "no index.html found" 86 | wRequested = "directory.html requested" 87 | wNotFound = "404 - file not found" 88 | 89 | type File = object 90 | name, html: string 91 | isDir: bool 92 | 93 | proc newFile(name, html: string, isDir: bool): File = 94 | result.name = name 95 | result.html = html 96 | result.isDir = isDir 97 | 98 | var workingPath = combineDir(dir, req.url.path) 99 | workingPath = workingPath.replace(re"%20", " ") 100 | 101 | req.logRequest() 102 | 103 | proc DirectoryHtmlRequested(query: seq[string]): bool = 104 | for q in query: 105 | if q.split(re"=")[0] == "ls": 106 | return true 107 | 108 | return false 109 | 110 | proc generateDirectoryDotHtml(warn: Warning, workingPath, rawReqPath: string): string = 111 | result = directory_html.replace("{warn}", $warn) 112 | result = result.replace("{path}", rawReqPath.replace(re"%20", " ")) 113 | var files: seq[File] 114 | var htmlToInsert: string 115 | 116 | for kind, path in walkDir(workingPath, true): 117 | if not showHidden and path[0] == '.': 118 | continue # Let's not show showHidden files if we aren't told to 119 | 120 | let truePath = combineDir(workingPath, path) # Path we need to use 121 | let reqPath = combineDir(rawReqPath, path) # Path the client uses 122 | let isDir = dirExists(truePath) # isDir field for file obj 123 | 124 | var html = file_html.replace("{file_path}", reqPath) 125 | html = html.replace("{file_name}", if isDir: path & '/' else: path) 126 | 127 | files.add(newFile(path, html, isDir)) 128 | 129 | files.sort do (x, y: File) -> int: 130 | if x.isDir and not y.isDir: 131 | -1 132 | elif y.isDir and not x.isDir: 133 | 1 134 | else: 135 | cmp(x.name.toLowerAscii, y.name.toLowerAscii) 136 | 137 | for f in files: 138 | htmlToInsert = htmlToInsert & f.html 139 | 140 | result = result.replace("{files}", htmlToInsert) 141 | 142 | if req.url.path.contains(".."): 143 | await req.respond(Http400, "Error 400: why are you using `..`?", 144 | headers = generateHeader("text/plain")) 145 | 146 | elif not (req.url.query == "") and 147 | DirectoryHtmlRequested(req.url.query.split("&")): 148 | if fileExists(workingPath): 149 | workingPath = parentDir(workingPath) 150 | await req.respond(Http200, generateDirectoryDotHtml(wRequested, workingPath, req.url.path), 151 | headers = generateHeader("text/html")) 152 | 153 | elif dirExists(workingPath): 154 | let indexPath = combineDir(workingPath, "/index.html") 155 | if fileExists(indexPath): 156 | 157 | await req.respond(Http200, readFile(indexPath), 158 | headers = generateHeader("text/html")) 159 | else: 160 | await req.respond(Http200, generateDirectoryDotHtml(wNoIndex, workingPath, req.url.path), 161 | headers = generateHeader("text/html")) 162 | 163 | elif fileExists(workingPath): 164 | let m = newMimeTypes() 165 | await req.respond(Http200, readFile(workingPath), 166 | headers = generateHeader(m.getMimeType(workingPath.splitFile.ext))) 167 | else: 168 | await req.respond(Http404, generateDirectoryDotHtml(wNotFound, 169 | workingPath.splitFile.dir, 170 | req.url.path.splitFile.dir), 171 | headers=generateHeader("text/html")) 172 | 173 | requestHandler 174 | 175 | proc main(input: Input, port: Natural, mime: string) = 176 | var mime = mime 177 | var server = newAsyncHttpServer() 178 | let mimedb = newMimetypes() 179 | 180 | echo "Starting server with IP ", $getPrimaryIPAddr(), " and port ", port 181 | case input.inputType: 182 | of itFile: 183 | if mime != "": 184 | mime = mimedb.getMimeType(mime) 185 | else: 186 | mime = mimedb.getMimeType(input.path.splitFile.ext) 187 | waitFor server.serve(Port(port), 188 | generateRequestHandlerFromFile(input.path, mime)) 189 | of itDir: 190 | waitFor server.serve(Port(port), 191 | generateRequestHandlerFromDirectory(input.path)) 192 | of itStdIn: 193 | mime = mimedb.getMimeType(mime) 194 | waitFor server.serve(Port(port), 195 | generateRequestHandlerFromString(input.str, mime)) 196 | 197 | when isMainModule: 198 | var port = 8080 199 | var stdinMode = false 200 | var filename: string 201 | var mime = "" 202 | 203 | var p = initOptParser(shortNoVal = {'i', 'h', 'q'}, 204 | longNoVal = @["stdin", "hidden", "quiet"]) 205 | for kind, key, val in p.getOpt(): 206 | case kind 207 | of cmdArgument: 208 | filename = key 209 | else: 210 | case key 211 | of "stdin", "i": 212 | stdinMode = true 213 | of "hidden", "h": 214 | showHidden = true 215 | of "quiet", "q": 216 | requestLogState = RequestLogState(rlType: rltNone) 217 | of "output": 218 | requestLogState = RequestLogState(rlType: rltFile, 219 | file: open(val, fmWrite)) 220 | of "port": 221 | discard parseInt(val, port) 222 | of "mime": 223 | mime = val 224 | else: # or `of "help"` 225 | if key != "help": echo fmt"I didn't understand option `{key}`!\n" 226 | echo help_text 227 | quit() 228 | 229 | if stdinMode: 230 | main(Input(inputType: itStdIn, str: stdin.readAll()), port.Natural, mime) 231 | else: 232 | if filename == "": 233 | echo "No filename passed in!\n" 234 | echo help_text 235 | elif fileExists(filename): 236 | main(Input(inputType: itFile, path: filename), port.Natural, mime) 237 | elif dirExists(filename): 238 | main(Input(inputType: itDir, path: filename), port.Natural, mime) 239 | else: 240 | echo fmt"File {filename} not found!" 241 | 242 | --------------------------------------------------------------------------------