In this article I will demonstrate how to create a simple web server in PowerShell.
There are a few parts to the solution:
- The code that listens for inbound requests on a specific host header and port (based on the HttpListener class). The domain and port can be set via $url.
- A function, named extract, to access the variables within a POST request (required when processing a form submission). The extracted variables are returned as a hash.
- A function, named render, to merge a hash with a block of HTML (where variables in the markup are wrapped within curly braces). For example, to generate
<p>Fred</p>
, you could call render with a template of<p>{name}</p>
and a hash of@{name = 'Fred'}
. Note: The function will attempt to replace a variable named page if called with a string instead of a hash. - A collection of routes (stored as a hash), where each route is identified by the combination of a HTTP method and URI. For example, a route of
'GET /foo' = { return do-foo }
will yield the output of a function named do-foo in response to a GET request for /foo.
The example provided below will start a web server listening at http://localhost:8080
, which when accessed via a browser, will display a HTML form with a single text input box and submit button. After entering a person's name, and clicking Submit, the web server will respond with a greeting (and a link back to the home page). Give it a shot.
# listening url.
$url = 'http://localhost:8080/'
$template = @'
<!DOCTYPE HTML>
<html>
<head>
<title>Example Web App</title>
<style type="text/css">
html, body, #container {height:95%}
body {font-family:verdana;line-height:1.5}
form, #container, p {align-items:center;display:flex;flex-direction:column;justify-content:center}
input {border:1px solid #999;border-radius:4px;margin-bottom:10px;padding:4px}
input[type=submit] {padding:6px 10px}
label, p {font-size:10px;padding-bottom:2px;text-transform:uppercase}
</style>
</head>
<body>
<div id="container">
<div id="content">
{page}
</div>
</div>
</body>
</html>
'@
$form = @'
<form method="post">
<label for="person">Name</label>
<input type="text" name="person" value="" required />
<input type="submit" name="submit" value="Submit" />
</form>
'@
$hello = @'
<p>Hello {name}.<br/><a href="/">Say hello again?</a></p>
'@
# request actions.
$routes = @{
'GET /' = { return (render $template $form) }
'POST /' = {
# get post data.
$data = extract $request
# get the submitted name.
$name = $data.item('person')
# render the 'hello' snippet, passing the name.
$page = render $hello @{name = $name}
# embed the snippet into the template.
return (render $template $page)
}
}
# embed content into the default template.
function render($template, $content) {
# shorthand for rendering the template.
if ($content -is [string]) { $content = @{page = $content} }
foreach ($key in $content.keys) {
$template = $template -replace "{$key}", $content[$key]
}
return $template
}
# get post data from the input stream.
function extract($request) {
$length = $request.contentlength64
$buffer = new-object "byte[]" $length
[void]$request.inputstream.read($buffer, 0, $length)
$body = [system.text.encoding]::ascii.getstring($buffer)
$data = @{}
$body.split('&') | %{
$part = $_.split('=')
$data.add($part[0], $part[1])
}
return $data
}
$listener = new-object system.net.httplistener
$listener.prefixes.add($url)
$listener.start()
while ($listener.islistening) {
$context = $listener.getcontext()
$request = $context.request
$response = $context.response
$pattern = "{0} {1}" -f $request.httpmethod, $request.url.localpath
$route = $routes.get_item($pattern)
if ($route -eq $null) {
$response.statuscode = 404
} else {
$content = & $route
$buffer = [system.text.encoding]::utf8.getbytes($content)
$response.contentlength64 = $buffer.length
$response.outputstream.write($buffer, 0, $buffer.length)
}
$response.close()
}