index.html (8306B)
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <link rel="stylesheet" href="/style.css" type="text/css"> 5 <meta charset="utf-8"> 6 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 7 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 8 <link rel="stylesheet" type="text/css" href="/style.css"> 9 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🏕️</text></svg>"> 10 <title></title> 11 </head> 12 <body> 13 <div id="page-wrapper"> 14 <div id="header" role="banner"> 15 <header class="banner"> 16 <div id="banner-text"> 17 <span class="banner-title"><a href="/">beauhilton</a></span> 18 </div> 19 </header> 20 <nav> 21 <a href="/about">about</a> 22 <a href="/now">now</a> 23 <a href="/thanks">thanks</a> 24 <a class="nav-active" href="/posts">posts</a> 25 <a href="https://notes.beauhilton.com">notes</a> 26 <a href="https://talks.beauhilton.com">talks</a> 27 <a href="https://git.beauhilton.com">git</a> 28 <a href="/contact">contact</a> 29 <a href="/atom.xml">rss</a> 30 </nav> 31 </div> 32 <main> 33 <h1> 34 password protecting static web pages 35 </h1> 36 <p> 37 <time id="post-date">2023-01-12</time> 38 </p> 39 <p id="post-excerpt"> 40 Static site generators, by and large, do not have support for password protecting individual pages. 41 This post shows a strategy that works for any SSG and does not require server access controls. 42 </p> 43 <p> 44 <a href="https://github.com/Greenheart/pagecrypt">PageCrypt</a> is 45 the tool I went with. It’s a TypeScript library that uses the native 46 <code>Web Crypto API</code> to encrypt html pages, stuff the encrypted 47 gobbledigook behind a simple password form, and decrypt the page when 48 the correct key is given. 49 </p> 50 <p> 51 I’m using the CLI version, but there are also native TypeScript and 52 Node.js versions. 53 </p> 54 <p> 55 The basic idea is this, taken from their docs: 56 </p> 57 <pre tabindex="0"><code class="language-sh">npx pagecrypt <span class="hl opt"><</span>src<span class="hl opt">> <</span>dest<span class="hl opt">> [</span>password<span class="hl opt">] [</span>options<span class="hl opt">]</span> 58 </code></pre> 59 <p> 60 Below is an excerpt from a <code>package.json</code> for a Hugo site 61 I maintain. First I build the site, then run the <code>postbuild</code> 62 command, which stitches together a piece of code that creates a list of 63 all the pages we’d like to encrypt and another bit that does the 64 encryption proper. 65 </p> 66 <pre tabindex="0"><code class="language-json"><span class="hl kwa">{</span> 67 <span class="hl kwc">"make-pw-files-file"</span><span class="hl opt">:</span> <span class="hl sng">"grep -rlF 'password_required: true' content | sed s+</span><span class="hl esc">\\</span><span class="hl sng">.md+</span><span class="hl esc">\\</span><span class="hl sng">/index.html+g | sed s+content/+public/+g > pw_file"</span><span class="hl opt">,</span> 68 <span class="hl kwc">"protect-files"</span><span class="hl opt">:</span> <span class="hl sng">"cat pw_file | while read f || [[ -n $f ]]; do npx pagecrypt $f $f $PAGECRYPT; done"</span><span class="hl opt">,</span> 69 <span class="hl kwc">"postbuild"</span><span class="hl opt">:</span> <span class="hl sng">"npm run -s make-pw-files-file && npm run -s protect-files "</span><span class="hl opt">,</span> 70 <span class="hl kwa">}</span> 71 </code></pre> 72 <br> 73 <h2> 74 <code>make-pw-files-file</code> 75 </h2> 76 <pre tabindex="0"><code class="language-sh"><span class="hl kwc">grep</span> <span class="hl kwb">-rlF</span> <span class="hl sng">'password_required: true'</span> content <span class="hl opt">|</span> <span class="hl kwc">sed</span> s<span class="hl opt">+</span><span class="hl esc">\\</span>.md<span class="hl opt">+</span><span class="hl esc">\\</span><span class="hl opt">/</span>index.html<span class="hl opt">+</span>g <span class="hl opt">|</span> <span class="hl kwc">sed</span> s<span class="hl opt">+</span>content<span class="hl opt">/+</span>public<span class="hl opt">/+</span>g <span class="hl opt">></span> pw_file 77 </code></pre> 78 <p> 79 This part uses a few standard Unix commands, the ubiquitous 80 <code>grep</code> and <code>sed</code>. 81 </p> 82 <p> 83 First it creates a list of markdown source files that have the header 84 <code>password_required: true</code>. 85 </p> 86 <p> 87 Here’s how that part works: 88 </p> 89 <pre tabindex="0"><code class="language-sh"><span class="hl kwc">grep</span> <span class="hl kwb">-rlF</span> <span class="hl sng">"text you're looking for"</span> path<span class="hl opt">/</span>to<span class="hl opt">/</span>files<span class="hl opt">/</span>of<span class="hl opt">/</span>interest 90 </code></pre> 91 <ul> 92 <li> 93 <code>-r</code> or <code>--recursive</code> searches though the 94 sub-directories of whichever path you gave it 95 </li> 96 <li> 97 <code>l</code> or <code>--files-with-matches</code> prints the 98 filenames of any files that match the search 99 </li> 100 <li> 101 <code>F</code> or <code>--fixed-strings</code> searches for the 102 exact string you ask for, not using regular expressions 103 </li> 104 </ul> 105 <p> 106 This will output something like: 107 </p> 108 <pre tabindex="0"><code class="language-sh">content<span class="hl opt">/</span>file0.md 109 content<span class="hl opt">/</span>file7.md 110 content<span class="hl opt">/</span>yet_another_file.md 111 </code></pre> 112 <p> 113 Next it passes that to <code>sed</code>, which replaces 114 <code>.md</code> with <code>/index.html</code>* resulting in: 115 </p> 116 <pre tabindex="0"><code class="language-sh">content<span class="hl opt">/</span>file<span class="hl opt">/</span>index.html 117 content<span class="hl opt">/</span>file<span class="hl num">7</span><span class="hl opt">/</span>index.html 118 content<span class="hl opt">/</span>yet_another_file<span class="hl opt">/</span>index.html 119 </code></pre> 120 <p> 121 * you’d have to adjust this command if you aren’t using a 122 page-per-folder style 123 </p> 124 <p> 125 Then it switches out <code>content/</code> with <code>public/</code>, 126 leading to the final list: 127 </p> 128 <pre tabindex="0"><code class="language-sh">public<span class="hl opt">/</span>file<span class="hl opt">/</span>index.html 129 public<span class="hl opt">/</span>file<span class="hl num">7</span><span class="hl opt">/</span>index.html 130 public<span class="hl opt">/</span>yet_another_file<span class="hl opt">/</span>index.html 131 </code></pre> 132 <p> 133 and sends those to a file. 134 </p> 135 <p> 136 Sending the list to a file is not necessary, could just pipe it again 137 to the stuff in <code>protect-files</code>, but I was getting itchy at 138 how long the command was getting. 139 </p> 140 <br> 141 <h2> 142 <code>protect-files</code> 143 </h2> 144 <pre tabindex="0"><code class="language-sh"><span class="hl kwc">cat</span> pw_file <span class="hl opt">|</span> <span class="hl kwa">while</span> <span class="hl kwb">read</span> f <span class="hl opt">|| [[</span> <span class="hl kwb">-n</span> <span class="hl kwd">$f</span> <span class="hl opt">]];</span> <span class="hl kwa">do</span> npx pagecrypt <span class="hl kwd">$f $f $PAGECRYPT</span><span class="hl opt">;</span> <span class="hl kwa">done</span> 145 </code></pre> 146 <p> 147 This part emits the contents of <code>pw_file</code>, then loops 148 through each line (<code>while read f || [[ -n $f ]]</code>) and 149 encrypts the corresponding file (<code>do npx pagecrypt</code>), finally 150 saving it back to itself (<code>$f $f</code>). 151 </p> 152 <p> 153 <code>$PAGECRYPT</code> is an environment variable that specifies the 154 password. 155 </p> 156 <p> 157 You could also set a password-per-page, either auto-generated (but 158 you’d have to figure out how to get that password to your users), or 159 using something like another header value in the <code>.md</code> files 160 that you grep for and save somewhere. For my use case, this is simpler 161 and scales well. 162 </p> 163 <br> 164 <h2> 165 and that’s it 166 </h2> 167 <p> 168 As always, <a href="../../contact">let me know</a> if you have any 169 questions or know of a better way. 170 </p> 171 </main> 172 <div id="footnotes"></div> 173 <footer></footer> 174 </div> 175 </body> 176 </html>