Pete Hinchley: Create a Web Server using PowerShell

In this article I will demonstrate how to create a simple web server in PowerShell.

There are a few parts to the solution:

  1. 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.
  2. 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.
  3. 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.
  4. 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()
}