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 |
--------------------------------------------------------------------------------