source code of /read/index.py

Last modified
Lines 1445

Parent directory Download CGIread sitemap Main page

Quick links: cat code contact content description download footer forms handle_injection_attempt if_none_match index_page is_injection_attempt isword ls main mk_description mk_navigation mk_referer_param navigation noindex ol_content redirect_spam sitemap syntax title

  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. root = '/var/www'
  4. owner = 'Oskar Skog'
  5. my_url = '/read/'
  6. canonical_url = 'https://oskog97.com/read/'
  7. html403file = '/var/www/oops/403.html'
  8. html404file = '/var/www/oops/404.html'
  9. html503file = '/var/www/oops/cgi503.html'
  10. import sys
  11. sys.path.append(root)
  12. import cgi
  13. import os
  14. import errno
  15. import compressout
  16. import base64
  17. import re
  18. import time
  19. import htmlescape
  20. import string
  21. import spammy
  22. import sitemap as mod_sitemap  # Name conflict with already existing function.
  23. import cgitb
  24. cgitb.enable()
  25. rootlen = len(root)
  26. #html_mime = 'text/html'      # Set to XHTML later.
  27. html_page = 'Content-Type: text/html; charset=UTF-8\n'  # Set to XHTML later.
  28. conf = eval(open('read.cfg').read())
  29. def redirect_spam(destination):
  30.     '''`destination` is the URL to which assholes should be redirected.'''
  31.     compressout.write_h('Status: 303\n')
  32.     compressout.write_h('Location: {}\n'.format(destination))
  33.     compressout.write_h('\n')
  34. def status400(message):
  35.     '''HTTP 400; `message` goes UNESCAPED inside a <pre> element.'''
  36.     compressout.write_h('Status: 400\n')
  37.     compressout.write_h(html_page)
  38.     compressout.write_h('\n')
  39.     compressout.write_b('''<!DOCTYPE html>
  40. <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
  41.     <head>
  42.         <meta charset="utf-8"/>
  43.         <meta name="viewport" content="width=device-width, initial-scale=1"/>
  44.         <link rel="stylesheet" href="https://oskog97.com/style.css" type="text/css"/>
  45.         <link rel="icon" type="image/png" href="/favicon.png"/>
  46.         <link rel="canonical" href="https://oskog97.com/read/"/>
  47. <!-- End html5 macro. -->
  48.         <title>400 - Bad Request</title>
  49.     </head>
  50.     <body>
  51.         
  52. <!-- BEGIN autogenerated navigation -->
  53. <nav><div id="navigation"><div id="nav_inner">
  54. <p><a href="#content" class="textonly">Skip navigation</a></p>
  55. <p class="row">
  56. <span class="textonly" translate="no">[</span><a class="head" href="/">Home</a><span class="textonly" translate="no">]</span>
  57. &gt;&gt;
  58. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/anonymine/">Anonymine</a><span class="textonly" translate="no">]</span>
  59. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/light-sensor/">Analog light sensor</a><span class="textonly" translate="no">]</span>
  60. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/PLLM-M702A/">Reverse-engineered schematics for PLLM-M702A</a><span class="textonly" translate="no">]</span>
  61. <span class="textonly" translate="no">[</span><a class="sub" href="/small-scripts/">Small scripts</a><span class="textonly" translate="no">]</span>
  62. </p>
  63. <p class="row">
  64. <span class="textonly" translate="no">[</span><a class="head" href="/small-scripts/">Small scripts</a><span class="textonly" translate="no">]</span>
  65. &gt;&gt;
  66. <span class="textonly" translate="no">]</span><span class="sub active">Website's scripts</span><span class="textonly" translate="no">[</span>
  67. </p>
  68. <p class="row">
  69. <span class="textonly" translate="no">[</span><a class="sub" href="/sitemap.py">Sitemap</a><span class="textonly" translate="no">]</span>
  70. </p>
  71. <hr class="textonly"/>
  72. </div></div></nav>
  73. <!-- END autogenerated navigation -->
  74.         <main><div id="content">
  75.             <h1 id="title">400 - Bad Request</h1>
  76.             <pre>{}</pre>
  77.             <p>
  78.                 Your request can't be understood.
  79.                 Check the parameters.
  80.             </p>
  81.             <p><a href="/read/">Documentation for the parameters</a></p>
  82.         </div></main>
  83. '''.format(message))
  84.     compressout.write_b('''
  85.         
  86. <!-- INCLUDED FOOTER -->
  87.     <hr class="textonly"/>
  88.     <p>
  89.         Copyright © Oskar Skog<br/>
  90.         Website content released under the <a
  91.         href="https://creativecommons.org/licenses/by/4.0/" rel="license noopener"
  92.         target="_blank">Creative Commons Attribution (CC-BY 4.0)</a> license
  93.         and my software usually under the <span class="a"><a target="_blank"
  94.         rel="noopener"
  95.         href="https://opensource.org/license/BSD-2-Clause">FreeBSD license
  96.         (2-clause)</a>.</span>
  97.         <br/>
  98.         Images may be from other sites, I should have cited useful sources
  99.         somewhere on the page.
  100.         <span class="notprint">Contact me if I haven't.</span>
  101.     </p>
  102.     <p id="contact" class="notprint">
  103.         You can contact me at: <a href="mailto:oskar@oskog97.com"
  104.         rel="noopener" target="_blank">oskar@oskog97.com</a>
  105.         <span class="a">(<a href="/pgp-pub/oskar.asc"
  106.                             >PGP public key</a>)</span>
  107.     </p>
  108.     <p> <a class="notprint" href="https://oskog97.com/read/?path=/style.css">
  109.             CSS Stylesheet
  110.         </a>
  111.     </p>
  112. </div></footer>
  113. <!-- END OF INCLUDED FOOTER -->
  114.     </body>
  115. </html>''')
  116. def status403():
  117.     '''HTTP 403'''
  118.     compressout.write_h(html_page)
  119.     compressout.write_h('Status: 403\n\n')
  120.     compressout.write_b(open(html403file).read())
  121. def status404():
  122.     '''HTTP 404'''
  123.     compressout.write_h('Status: 404\n')
  124.     compressout.write_h(html_page)
  125.     compressout.write_h('\n')
  126.     compressout.write_b(open(html404file).read())
  127. def status503():
  128.     '''
  129.     HTTP 503
  130.     
  131.     Call this if there is too much load on the server to do something.
  132.     (Used by the sitemap function.)
  133.     '''
  134.     compressout.write_h('Status: 503\n')
  135.     compressout.write_h(html_page)
  136.     # One factor is load avg for 1 minute, add some slop to the delay for bots.
  137.     compressout.write_h('Retry-After: 90\n')
  138.     compressout.write_h('\n')
  139.     compressout.write_b(open(html503file).read())
  140. def index_page():
  141.     '''https://oskog97.com/read/'''
  142.     # Handle 304s.
  143.     ETag = '"{}{}{}"'.format(
  144.         'x'*('application/xhtml+xml' in html_page),
  145.         'z'*('gzip' in os.getenv('HTTP_ACCEPT_ENCODING', '')),
  146.         os.stat('index.py').st_mtime,
  147.     )
  148.     compressout.write_h('Vary: If-None-Match\n')
  149.     compressout.write_h('ETag: {}\n'.format(ETag))
  150.     compressout.write_h(html_page)
  151.     if os.getenv('HTTP_IF_NONE_MATCH') == ETag:
  152.         compressout.write_h('Status: 304\n\n')
  153.         return
  154.     compressout.write_h('\n')
  155.     if os.getenv('REQUEST_METHOD') == 'HEAD':
  156.         return
  157.     # Write out a static page.
  158.     compressout.write_b('''<!DOCTYPE html>
  159. <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
  160.     <head>
  161.         <meta charset="utf-8"/>
  162.         <meta name="viewport" content="width=device-width, initial-scale=1"/>
  163.         <link rel="stylesheet" href="https://oskog97.com/style.css" type="text/css"/>
  164.         <link rel="icon" type="image/png" href="/favicon.png"/>
  165.         <link rel="canonical" href="https://oskog97.com/read/"/>
  166. <!-- End html5 macro. -->
  167.     <!-- With canonical link tag. -->
  168.         <link rel="stylesheet" type="text/css" href="/read/style.css"/>
  169.         <meta name="description" content="Interested in the scripts I have
  170.         on my website? Come and take a look at them."/>
  171.         <title>Website's scripts</title>
  172.     </head>
  173.     <body>
  174.         
  175. <!-- BEGIN autogenerated navigation -->
  176. <nav><div id="navigation"><div id="nav_inner">
  177. <p><a href="#content" class="textonly">Skip navigation</a></p>
  178. <p class="row">
  179. <span class="textonly" translate="no">[</span><a class="head" href="/">Home</a><span class="textonly" translate="no">]</span>
  180. &gt;&gt;
  181. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/anonymine/">Anonymine</a><span class="textonly" translate="no">]</span>
  182. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/light-sensor/">Analog light sensor</a><span class="textonly" translate="no">]</span>
  183. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/PLLM-M702A/">Reverse-engineered schematics for PLLM-M702A</a><span class="textonly" translate="no">]</span>
  184. <span class="textonly" translate="no">[</span><a class="sub" href="/small-scripts/">Small scripts</a><span class="textonly" translate="no">]</span>
  185. </p>
  186. <p class="row">
  187. <span class="textonly" translate="no">[</span><a class="head" href="/small-scripts/">Small scripts</a><span class="textonly" translate="no">]</span>
  188. &gt;&gt;
  189. <span class="textonly" translate="no">]</span><span class="sub active">Website's scripts</span><span class="textonly" translate="no">[</span>
  190. </p>
  191. <p class="row">
  192. <span class="textonly" translate="no">[</span><a class="sub" href="/sitemap.py">Sitemap</a><span class="textonly" translate="no">]</span>
  193. </p>
  194. <hr class="textonly"/>
  195. </div></div></nav>
  196. <!-- END autogenerated navigation -->
  197.         <main><div id="content">
  198.             <h1 id="title">Website's scripts</h1>
  199.     ''')
  200.     compressout.write_b('''
  201.             <p>
  202.                 Interested in the scripts I have on my website?
  203.                 Go take a look at them; start crawling the
  204.                 <a href="{0}?path=/">root directory</a> or take a look
  205.                 at the <span class="a"><a href="{0}?sitemap=html"
  206.                 >(sub)sitemap</a>.</span>
  207.             </p>
  208.             <div id="syntax">
  209.                 <h2>Parameter syntax</h2>
  210.                 <p>
  211.                     Descriptions for the parameters can be found in
  212.                     the request forms.
  213.                 </p>
  214.                 <ul>
  215.                     <li>
  216.                         Asterisks <q>*</q> represent a value that can be
  217.                         (almost) anything.
  218.                     </li>
  219.                     <li>Square brackets <q>[]</q> represent optional.</li>
  220.                     <li>Curly brackets <q>&#x7b;&#x7d;</q> represent mandatory.</li>
  221.                     <li>Pipes <q>|</q> represent either or.</li>
  222.                 </ul>
  223.                 <p>There are three acceptable "sets" of parameters:</p>
  224.                 <ol>
  225. <li><pre>{0}?sitemap=&#x7b;html|xml&#x7d;</pre></li>
  226. <li><pre>{0}?path=*[&amp;download=yes]</pre></li>
  227. <li><pre>{0}?path=*[&amp;referer=*[&amp;title=*]]</pre></li>
  228.                 </ol>
  229.                 <p>
  230.                     The order of the valid parameters doesn't matter, but
  231.                     this is the recommended/canonical order.
  232.                 </p>
  233.             </div>
  234.             <div id="forms">
  235.                 <h2>Request forms</h2>
  236.                 <p><strong>
  237.                     Notice that these are three different forms.
  238.                 </strong></p>
  239.                 <form action="{0}" method="get">
  240.                 <h3>Sitemap</h3>
  241.                 <p>
  242.                     The <code>sitemap</code> parameter can be either
  243.                     <q><code>html</code></q>, <q><code>xml</code></q>
  244.                     or the default <q><code>none</code></q>.
  245.                     It can't be used together with any other parameters.
  246.                 </p>
  247.                 <p>
  248.                     <input type="radio" name="sitemap" value="html"/>
  249.                     Request an HTML sitemap instead of a page<br/>
  250.                     <input type="radio" name="sitemap" value="xml"/>
  251.                     request an XML sitemap instead of a page<br/>
  252.                     <input type="submit"/>
  253.                 </p>
  254.                 </form>
  255.                 <form action="{0}" method="get">
  256.                 <h3>Page</h3>
  257.                 <p>
  258.                     A page (source code of a CGI script) is selected with the
  259.                     <code>path</code> parameter.  The value of the
  260.                     <code>path</code> parameter is a URL relative to this
  261.                     site, ie. an URL beginning with a single slash.
  262.                 </p>
  263.                 <p>
  264.                     The <code>path</code> is the site-local URL to the CGI
  265.                     script or directory you're interested in.  If you set the
  266.                     value to <q><code>/read/index.py</code></q>, you'll get the
  267.                     source code for this script. And if you set it to
  268.                     <q><code>/</code></q>, you'll get a directory listing
  269.                     of the site's root directory.
  270.                 </p>
  271.                 <p>
  272.                     Path/URL: <input type="text" name="path" value="/"/>
  273.                     <input type="submit"/><br/>
  274.                     <input type="checkbox" name="download" value="yes"/>
  275.                     Download / see it as plain text
  276.                     
  277.                 </p>
  278.                 <p>
  279.                     The <code>download</code> parameter can be set to either
  280.                     <q><code>yes</code></q> or the default
  281.                     <q><code>no</code></q>.  The download option does
  282.                     obviously not work with directories.
  283.                 </p>
  284.                 </form>
  285.                 <form action="{0}" method="get">
  286.                 <h3>Link back to a referencing page</h3>
  287.                 <p>
  288.                     If <code>download</code> is <q><code>no</code></q> or
  289.                     unset and a page (not a sitemap) was requested, it is
  290.                     possible to change the navigation to make the requested
  291.                     page link back to a referring page.
  292.                 </p>
  293.                 <p>
  294.                     The <code>referer</code> (yes, misspelled like the HTTP
  295.                     Referer) parameter is the URL of the referencing page.
  296.                     (Don't try to specify a site that isn't mine.)
  297.                     The <code>title</code> parameter gives the back link a
  298.                     different text than <q>Back</q>.
  299.                 </p>
  300.                 <table>
  301.                     <tr>
  302.                         <th><code>path</code></th>
  303.                         <td><input type="text" name="path" value="/"/></td>
  304.                     </tr>
  305.                     <tr>
  306.                         <th><code>referer</code></th>
  307.                         <td><input type="text" name="referer"/></td>
  308.                     </tr>
  309.                     <tr>
  310.                         <th><code>title</code></th>
  311.                         <td><input type="text" name="title"/></td>
  312.                     </tr>
  313.                     <tr>
  314.                         <td></td>
  315.                         <td><input type="submit"/></td>
  316.                     </tr>
  317.                 </table>
  318.                 </form>
  319.             </div>
  320.         </div></main>
  321.     '''.format(my_url))
  322.     compressout.write_b('''
  323.         
  324. <!-- INCLUDED FOOTER -->
  325. <footer><div id="footer">
  326.     <hr class="textonly"/>
  327.     <p>
  328.         Copyright © Oskar Skog<br/>
  329.         Website content released under the <a
  330.         href="https://creativecommons.org/licenses/by/4.0/" rel="license noopener"
  331.         target="_blank">Creative Commons Attribution (CC-BY 4.0)</a> license
  332.         and my software usually under the <span class="a"><a target="_blank"
  333.         rel="noopener"
  334.         href="https://opensource.org/license/BSD-2-Clause">FreeBSD license
  335.         (2-clause)</a>.</span>
  336.         <br/>
  337.         Images may be from other sites, I should have cited useful sources
  338.         somewhere on the page.
  339.         <span class="notprint">Contact me if I haven't.</span>
  340.     </p>
  341.     <p id="contact" class="notprint">
  342.         You can contact me at: <a href="mailto:oskar@oskog97.com"
  343.         rel="noopener" target="_blank">oskar@oskog97.com</a>
  344.         <span class="a">(<a href="/pgp-pub/oskar.asc"
  345.                             >PGP public key</a>)</span>
  346.     </p>
  347.     <p> <a class="notprint" href="https://oskog97.com/read/?path=/style.css">
  348.             CSS Stylesheet
  349.         </a>
  350.     </p>
  351. </div></footer>
  352. <!-- END OF INCLUDED FOOTER -->
  353.     </body>
  354. </html>
  355. ''')
  356. def noindex(path):
  357.     '''
  358.     Returns True if `path` should be noindexed.
  359.     
  360.     `path` is an absolute **filesystem** path.
  361.     '''
  362.     def isword(w):
  363.         letters = string.ascii_letters + ',.'
  364.         for ch in w:
  365.             if w not in letters:
  366.                 return False
  367.         return True
  368.     # 1. White list
  369.     # 2. Black list
  370.     # 3. Page quality (not applicable for directories)
  371.     
  372.     # Check whitelist first.
  373.     for regex in conf['doindex']:
  374.         if re.match(regex, path[rootlen:]) is not None:
  375.             return False
  376.             break
  377.     
  378.     # Blacklist (two kinds):
  379.     # - Generated from another file.
  380.     # - Explicitly blacklisted in 'read.cfg'.
  381.     for match, replace in conf['madefrom']:
  382.         if re.match(match, path[rootlen:]) is not None:
  383.             try:
  384.                 os.stat(root + re.sub(match, replace, path[rootlen:]))
  385.                 return True
  386.             except:
  387.                 pass
  388.     for regex in conf['noindex'] + conf['hide']:
  389.         if re.match(regex, path[rootlen:]) is not None:
  390.             return True
  391.     
  392.     # Quality:
  393.     #   - Text file
  394.     #   - At least 3072 Unicode code points
  395.     #   - At least 300 words
  396.     #   - At least 60 lines
  397.     #   - Half the limitations if a meta description and title is found
  398.     #   - A third of the limimitations if an onpage description is found
  399.     try:
  400.         os.listdir(path)
  401.         return False
  402.     except:
  403.         pass
  404.     # Normal file.
  405.     try:
  406.         if sys.version_info[0] > 2:
  407.             text = open(path).read()
  408.         else:
  409.             text = open(path).read().decode('utf-8')
  410.     except:
  411.         return True
  412.     min_chars, min_words, min_lines, min_comments = 3072, 300, 60, 24
  413.     quality = mk_description(path)[0] + 1
  414.     min_chars //= quality; min_words //= quality
  415.     min_lines //= quality; min_comments //= quality
  416.     if len(text) < min_chars:
  417.         return True
  418.     if text.count('\n') + 1 < min_lines:
  419.         return True
  420.     n_comments = 0
  421.     is_comment = re.compile('^(.*#.*| *\\* .*|.*<!--.*|.*\'\'\'.*)$')
  422.     for line in text.split('\n'):
  423.         if re.match(is_comment, line) is not None:
  424.             n_comments += 1
  425.     if n_comments < min_comments:
  426.         return True
  427.     if len(list(filter(isword, text.replace('\n', ' ').split(' ')))) < min_words:
  428.         return True
  429.     # Passed the quality tests:
  430.     return False
  431. def mk_navigation(referer, title):
  432.     '''
  433.     Returns a string which is the navigation bar's HTML.
  434.     
  435.     `title` is the title of the requested page.
  436.     
  437.     `referer` is used to **optionally** ``integrate`` a page.
  438.     `referer` is a tuple of (URL, title) for the "back" link.
  439.     '''
  440.     if referer[0]:
  441.         return htmlescape.escape('''<!-- Navigation generated by CGIread. -->
  442. <nav><div id="navigation"><div id="nav_inner">
  443. <p><a href="#content" class="textonly">Skip navigation</a></p>
  444. <p class="row">
  445. <span class="textonly" translate="no">[</span><a class="head" href="{URL}">{title}</a><span class="textonly" translate="no">]</span>
  446. &gt;&gt;
  447. <span class="textonly" translate="no">]</span><span class="sub active">{me}</span><span class="textonly" translate="no">[</span>
  448. <span class="textonly" translate="no">[</span><a class="sub" href="{my_url}?sitemap=html">Sitemap for website's scripts</a><span class="textonly" translate="no">]</span>
  449. </p>
  450. <p class="row">
  451. <span class="textonly" translate="no">[</span><a class="head" href="/">Home</a><span class="textonly" translate="no">]</span>
  452. &gt;&gt;
  453. <span class="textonly" translate="no">[</span><a class="sub" href="/read/">Website's scripts</a><span class="textonly" translate="no">]</span>
  454. <span class="textonly" translate="no">[</span><a class="sub" href="/pages/policy.html">Privacy policy &amp; terms of use</a><span class="textonly" translate="no">]</span>
  455. <span class="textonly" translate="no">[</span><a class="sub" href="/sitemap.py">Sitemap</a><span class="textonly" translate="no">]</span>
  456. </p>
  457. <hr class="textonly"/>
  458. </div></div></nav>
  459. <!-- End of navigation. -->''',
  460.             URL=(2, referer[0]),
  461.             title=(1, referer[1]),
  462.             me=(1, title),
  463.             my_url=(0, my_url),
  464.         )
  465.     else:
  466.         return '''
  467. <!-- BEGIN autogenerated navigation -->
  468. <nav><div id="navigation"><div id="nav_inner">
  469. <p><a href="#content" class="textonly">Skip navigation</a></p>
  470. <p class="row">
  471. <span class="textonly" translate="no">[</span><a class="head" href="/">Home</a><span class="textonly" translate="no">]</span>
  472. &gt;&gt;
  473. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/anonymine/">Anonymine</a><span class="textonly" translate="no">]</span>
  474. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/light-sensor/">Analog light sensor</a><span class="textonly" translate="no">]</span>
  475. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/PLLM-M702A/">Reverse-engineered schematics for PLLM-M702A</a><span class="textonly" translate="no">]</span>
  476. <span class="textonly" translate="no">[</span><a class="sub" href="/small-scripts/">Small scripts</a><span class="textonly" translate="no">]</span>
  477. </p>
  478. <p class="row">
  479. <span class="textonly" translate="no">[</span><a class="head" href="/small-scripts/">Small scripts</a><span class="textonly" translate="no">]</span>
  480. &gt;&gt;
  481. <span class="textonly" translate="no">]</span><span class="sub active">Website's scripts</span><span class="textonly" translate="no">[</span>
  482. </p>
  483. <p class="row">
  484. <span class="textonly" translate="no">[</span><a class="sub" href="/sitemap.py">Sitemap</a><span class="textonly" translate="no">]</span>
  485. </p>
  486. <hr class="textonly"/>
  487. </div></div></nav>
  488. <!-- END autogenerated navigation -->
  489. '''
  490. def mk_referer_param(referer):
  491.     '''Returns one of:
  492.         ''
  493.         '&referer=' + referer[0]
  494.         '&referer=' + referer[0] + '&title=' + referer[1]
  495.     to be added to links from the requested page.
  496.     
  497.     `referer` is used to **optionally** ``integrate`` a page.
  498.     See `mk_navigation`
  499.     '''
  500.     if referer[0]:
  501.         if referer[1] != 'Back':
  502.             title = '&title={}'.format(referer[1])
  503.         else:
  504.             title = ''
  505.         return '&referer={}{}'.format(referer[0], title)
  506.     else:
  507.         return ''
  508. def mk_description(path):
  509.     '''
  510.     Return three strings: (good, title, meta_description, onpage_description)
  511.     
  512.     `path` is the absolute filesystem path to the requested page.
  513.     
  514.     `good` is
  515.         0       no title and description
  516.         1       title and meta description only
  517.         2       also an onpage description
  518.     
  519.     `title` is the title of the page.
  520.     
  521.     `meta_description` is the content of the description meta tag.
  522.     
  523.     `onpage_description` is HTML content for the onpage description.
  524.     requested page.
  525.     '''
  526.     good = 0
  527.     title = "source code of {}".format(path[rootlen:])
  528.     meta_description = ''
  529.     onpage_description = None
  530.     try:
  531.         content = open(path + '.info').read().split('\n')
  532.         good = 1
  533.     except:
  534.         pass
  535.     if good:
  536.         title = content[0]
  537.         try:
  538.             sep = content.index('.')
  539.         except ValueError:
  540.             sep = None
  541.         if sep is not None:
  542.             good = 2
  543.             meta_description = '\n'.join(content[1:sep])
  544.             onpage_description = '\n'.join(content[sep+1:])
  545.         else:
  546.             meta_description = '\n'.join(content[1:])
  547.     if onpage_description is None:
  548.         onpage_description = htmlescape.escape('<p>{}</p>',1,meta_description)
  549.     return good, title, meta_description, onpage_description
  550. def sitemap(sitemap_type):
  551.     '''
  552.     Write out an XML or HTML sitemap.
  553.     sitemap_type in ('xml', 'html')
  554.     
  555.     The XML sitemap will exclude entries from `conf['noxmlsitemap']`.
  556.     '''    
  557.     
  558.     if os.getenv('REQUEST_METHOD') != 'HEAD': # NOTICE
  559.         # Prevent over-revving the server.
  560.         # HEAD requests are basically no-ops.
  561.         maxload = conf['sitemap-maxload']
  562.         if os.getloadavg()[0] > maxload['load-avg1']:
  563.             status503()
  564.             return
  565.         try:
  566.             access_times = list(map(
  567.                 float, open('read.throttlecontrol').read().strip().split(':')
  568.             ))
  569.         except:
  570.             access_times = [0]
  571.         if time.time() - access_times[-1] < maxload['throttle-time']:
  572.             status503()
  573.             return
  574.         access_times.insert(0, time.time())
  575.         access_times = access_times[:maxload['throttle-requests']]
  576.         f = open('read.throttlecontrol', 'w')
  577.         f.write(':'.join(list(map(str, access_times))) + '\n')
  578.         f.close()
  579.     # Write headers before doing anything else.
  580.     # A HEAD request doesn't need to know the length (it's TE chunked).
  581.     if sitemap_type == 'xml':
  582.         compressout.write_h('Content-Type: application/xml; charset=UTF-8\n')
  583.         compressout.write_h(
  584.             'Link: <{my_url}?sitemap=html>'.format(my_url=canonical_url) +
  585.             '; rel="canonical"' +
  586.             '; type="text/html"\n'
  587.         )
  588.         compressout.write_h('X-Robots-Tag: noindex\n\n') # NOTE: last.
  589.     elif sitemap_type == 'html':
  590.         compressout.write_h(html_page)
  591.         compressout.write_h('\n')
  592.     else:
  593.         assert False, "Neither 'xml' nor 'html'"
  594.     if os.getenv('REQUEST_METHOD') == 'HEAD': # NOTICE
  595.         return
  596.     
  597.     # Find the pages worth being in the sitemap.
  598.     no_access = conf['noaccess'] + conf['hide'] + conf['topsecret']
  599.     paths = []
  600.     
  601.     for basedir, dirs, files in os.walk(root, topdown=True):
  602.         # Exclude hidden directories:
  603.         remove_list = []
  604.         sys.stderr.write('In {}\n'.format(basedir))
  605.         sys.stderr.write('Dirs: {}\n'.format(repr(dirs)))
  606.         for dirname in dirs:
  607.             dirpath = os.path.join(basedir, dirname)[rootlen:]
  608.             for regex in no_access:
  609.                 if re.match(regex, dirpath) is not None:
  610.                     #dirs.remove(dirname)
  611.                     # BUG: The for loop will skip items in the list if
  612.                     # other items are removed while looping.
  613.                     # This caused some real' nasty stuff like sshin to
  614.                     # be crawled, took a whopping .65 seconds.
  615.                     remove_list.append(dirname)
  616.                     break
  617.         sys.stderr.write('Removed dirs: {}\n'.format(repr(remove_list)))
  618.         for dirname in remove_list:
  619.             dirs.remove(dirname)
  620.         
  621.         # Iterate over files:
  622.         for filename in files:
  623.             filepath = os.path.join(basedir, filename)
  624.             # No symlinks allowed.
  625.             #if os.stat(filepath).st_mode == os.lstat(filepath).st_mode:
  626.             if not os.path.islink(filepath):
  627.                 #try:
  628.                     description = mk_description(filepath)
  629.                     if description[0]:
  630.                         # Only indexable content allowed.
  631.                         if not noindex(filepath):
  632.                             paths.append((filepath[rootlen:], description[3]))
  633.                         else:
  634.                             sys.stderr.write('{} is noindexed\n'.format(filepath))
  635.                     else:
  636.                         sys.stderr.write('{} has no description\n'.format(filepath))
  637.                 #except IOError as error:
  638.                     #assert error.errno in (
  639.                         #errno.EISDIR, errno.EACCES
  640.                     #), error.errno
  641.             else:
  642.                 sys.stderr.write('{} is link\n'.format(filepath))
  643.     
  644.     paths.sort(key=lambda x: x[0])
  645.     
  646.     # Print the body.
  647.     if sitemap_type == 'xml':
  648.         compressout.write_b('''<?xml version="1.0" encoding="UTF-8"?>
  649. <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  650. ''')
  651.         #
  652.         for path, description in paths:
  653.             # Loop through all the regexes:
  654.             for regex in conf['noxmlsitemap']:
  655.                 if re.match(regex, path) is not None:
  656.                     break
  657.             else:
  658.                 compressout.write_b(htmlescape.escape('''<url>
  659.     <loc>{canonical_url}?path={path}</loc>
  660.     <priority>0.5</priority>
  661. ''',
  662.                     canonical_url=(0, canonical_url),
  663.                     path=(1, path),
  664.                 ))
  665.                 mod_sitemap.lastmod_changefreq(
  666.                     root + path,
  667.                     compressout,
  668.                 )
  669.                 compressout.write_b('</url>\n')
  670.         #
  671.         compressout.write_b('</urlset>\n')
  672.     elif sitemap_type == 'html':
  673.         compressout.write_b('''<!DOCTYPE html>
  674. <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
  675.     <head>
  676.         <meta charset="utf-8"/>
  677.         <meta name="viewport" content="width=device-width, initial-scale=1"/>
  678.         <link rel="stylesheet" href="https://oskog97.com/style.css" type="text/css"/>
  679.         <link rel="icon" type="image/png" href="/favicon.png"/>
  680. <!-- End html5nc macro. -->
  681.         <link rel="canonical" href="{canonical_url}?sitemap=html"/>
  682.         <link rel="alternate" href="{canonical_url}?sitemap=xml"
  683.             type="application/xml"/>
  684.         <meta name="robots" content="noindex, follow"/>
  685.         <title>Sitemap for scripts' source code</title>
  686.         <meta name="description" content="
  687.             Sitemap of all scripts available through /read/.
  688.         "/>
  689.     </head>
  690.     <body>
  691.         
  692. <!-- BEGIN autogenerated navigation -->
  693. <nav><div id="navigation"><div id="nav_inner">
  694. <p><a href="#content" class="textonly">Skip navigation</a></p>
  695. <p class="row">
  696. <span class="textonly" translate="no">[</span><a class="head" href="/">Home</a><span class="textonly" translate="no">]</span>
  697. &gt;&gt;
  698. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/anonymine/">Anonymine</a><span class="textonly" translate="no">]</span>
  699. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/light-sensor/">Analog light sensor</a><span class="textonly" translate="no">]</span>
  700. <span class="textonly" translate="no">[</span><a class="sub" href="/projects/PLLM-M702A/">Reverse-engineered schematics for PLLM-M702A</a><span class="textonly" translate="no">]</span>
  701. <span class="textonly" translate="no">[</span><a class="sub" href="/small-scripts/">Small scripts</a><span class="textonly" translate="no">]</span>
  702. </p>
  703. <p class="row">
  704. <span class="textonly" translate="no">[</span><a class="head" href="/small-scripts/">Small scripts</a><span class="textonly" translate="no">]</span>
  705. &gt;&gt;
  706. <span class="textonly" translate="no">]</span><span class="sub active">Website's scripts</span><span class="textonly" translate="no">[</span>
  707. </p>
  708. <p class="row">
  709. <span class="textonly" translate="no">[</span><a class="sub" href="/sitemap.py">Sitemap</a><span class="textonly" translate="no">]</span>
  710. </p>
  711. <hr class="textonly"/>
  712. </div></div></nav>
  713. <!-- END autogenerated navigation -->
  714.         <main><div id="content" class="sitemap">
  715.             <h1 id="title">Sitemap for scripts' source code</h1>
  716.             <p><a href="{my_url}?path=/">Root directory</a></p>
  717.             <dl>
  718. '''.format(my_url=my_url, canonical_url=canonical_url))
  719.         #
  720.         indent = 16 * ' '
  721.         for path, description in paths:
  722.             compressout.write_b(indent + htmlescape.escape(
  723.                 '''<dt><a translate="no" href="{my_url}?path={path}">
  724.                     {path}
  725.                 </a></dt>\n''',
  726.                 path=(0, path),
  727.                 my_url=(0, canonical_url),
  728.             ))
  729.             compressout.write_b(indent +
  730.                 htmlescape.escape('<dd>{}</dd>\n', 0, description)
  731.             )
  732.         #
  733.         compressout.write_b('''            </dl>
  734.         </div></main>
  735.         
  736. <!-- INCLUDED FOOTER -->
  737. <footer><div id="footer">
  738.     <hr class="textonly"/>
  739.     <p>
  740.         Copyright © Oskar Skog<br/>
  741.         Website content released under the <a
  742.         href="https://creativecommons.org/licenses/by/4.0/" rel="license noopener"
  743.         target="_blank">Creative Commons Attribution (CC-BY 4.0)</a> license
  744.         and my software usually under the <span class="a"><a target="_blank"
  745.         rel="noopener"
  746.         href="https://opensource.org/license/BSD-2-Clause">FreeBSD license
  747.         (2-clause)</a>.</span>
  748.         <br/>
  749.         Images may be from other sites, I should have cited useful sources
  750.         somewhere on the page.
  751.         <span class="notprint">Contact me if I haven't.</span>
  752.     </p>
  753.     <p id="contact" class="notprint">
  754.         You can contact me at: <a href="mailto:oskar@oskog97.com"
  755.         rel="noopener" target="_blank">oskar@oskog97.com</a>
  756.         <span class="a">(<a href="/pgp-pub/oskar.asc"
  757.                             >PGP public key</a>)</span>
  758.     </p>
  759.     <p> <a class="notprint" href="https://oskog97.com/read/?path=/style.css">
  760.             CSS Stylesheet
  761.         </a>
  762.     </p>
  763. </div></footer>
  764. <!-- END OF INCLUDED FOOTER -->
  765.     </body>
  766. </html>
  767. ''')
  768.     else:
  769.         assert False, "Neither 'xml' nor 'html'"
  770. def ls(path, referer):
  771.     '''
  772.     '''
  773.     compressout.write_h(html_page)
  774.     compressout.write_h('\n')
  775.     if os.getenv('REQUEST_METHOD') == 'HEAD':
  776.         return
  777.     compressout.write_b('''<!DOCTYPE html>
  778. <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
  779.     <head>
  780.         <meta charset="utf-8"/>
  781.         <meta name="viewport" content="width=device-width, initial-scale=1"/>
  782.         <link rel="stylesheet" href="https://oskog97.com/style.css" type="text/css"/>
  783.         <link rel="icon" type="image/png" href="/favicon.png"/>
  784. <!-- End html5nc macro. -->
  785. ''')
  786.     compressout.write_b(htmlescape.escape('''
  787.         <link rel="stylesheet" type="text/css" href="/read/style.css"/>
  788.         <title>Index of {name}</title>
  789.         <meta name="robots" content="{robots_follow}, noindex"/>
  790.         <link rel="canonical" href="{canonical_url}?path={name}"/>
  791.     </head>
  792.     <body>
  793.         {navigation}
  794.         <main><div id="content" class="ls">
  795.             <h1 id="title">Index of <span translate="no">{name}</span></h1>
  796.             <p class="read-nav">
  797.                 {isroot_commentout_start}
  798.                     <a href="{my_url}?path={parent_path}{referer_params}">
  799.                         Parent directory
  800.                     </a>
  801.                 {isroot_commentout_end}
  802.                 <a href="{my_url}?sitemap=html">CGIread sitemap</a>
  803.                 <a href="{my_url}">Main page</a>
  804.             </p>
  805.             <table id="ls">
  806.             ''',
  807.             name          =(1, path[rootlen:] + '/'),
  808.             parent_path   =(2, '/'.join(path.split('/')[:-1])[rootlen:]+'/'),
  809.             robots_follow =(2, 'no'*noindex(path)+'follow'),
  810.             navigation    =(0, mk_navigation(
  811.                                 referer,
  812.                                 "Index of "+path[rootlen:]+'/'
  813.                             )),
  814.             referer_params=(2, mk_referer_param(referer)),
  815.             my_url=(0, my_url),
  816.             canonical_url=(0, canonical_url),
  817.             isroot_commentout_start=(0, '<!--'*(path == root)),
  818.             isroot_commentout_end=(0, '-->'*(path == root)),
  819.         ))
  820.     no_access = conf['noaccess'] + conf['hide'] + conf['topsecret']
  821.     
  822.     for x in sorted(os.listdir(path)):
  823.         full_path = os.path.join(path, x)
  824.         
  825.         forbidden = False
  826.         for regex in no_access:
  827.             if re.match(regex, full_path[rootlen:]) is not None:
  828.                 forbidden = True
  829.                 break
  830.         if forbidden:
  831.             continue
  832.         
  833.         #url = cgi.escape(full_path, quote=True)
  834.         try:
  835.             os.listdir(full_path)
  836.             is_dir = 1
  837.         except:
  838.             is_dir = 0
  839.         # mobile_desc
  840.         # desktop_desc
  841.         if is_dir:
  842.            mobile_desc = '<span class="yeah">-&gt;</span>'
  843.            desktop_desc = '<span class="yeah">Directory</span>'
  844.         else:
  845.             try:
  846.                 content = open(full_path).read()        # This fails on Python 3 !!!
  847.                 if sys.version_info[0] == 2:
  848.                     content.decode('UTF-8')
  849.                 binary = False
  850.             except:
  851.                 binary = True
  852.             if binary:
  853.                 desktop_desc = 'Binary'
  854.                 mobile_desc = ':-('
  855.             else:
  856.                 good, title, meta_d, onpage_d = mk_description(full_path)
  857.                 if good == 2:
  858.                     desktop_desc = htmlescape.escape(
  859.                         '<span class="thenumberofthebeast">{}</span>',
  860.                         1, meta_d
  861.                     )
  862.                     if noindex(full_path):
  863.                         mobile_desc = '<span class="yeah">:-)</span>'
  864.                     else:
  865.                         mobile_desc = '<span class="thenumberofthebeast">:-D</span>'
  866.                 elif not noindex(full_path):
  867.                     mobile_desc = '<span class="yeah">:-)</span>'
  868.                     if compressout.debug_cookie:
  869.                         desktop_desc = '<span class="yeah">Text; indexable</span>'
  870.                     else:
  871.                         desktop_desc = '<span class="yeah">Text</span>'
  872.                 else:
  873.                     mobile_desc = ':-|'
  874.                     if compressout.debug_cookie:
  875.                         desktop_desc = 'Boring; unindexable'
  876.                     else:
  877.                         desktop_desc = 'Looks boring'
  878.                     
  879.         compressout.write_b(
  880.             htmlescape.escape(
  881.                 '''<tr><td class="mobile">{mobile_desc}</td>
  882.                 <td><a translate="no"
  883.                     href="{site}?path={path}{referer}">{text}</a></td>
  884.                 <td class="desktop">{desktop_desc}</td></tr>
  885.                 ''',
  886.                 site=(0, my_url),
  887.                 path=(2, full_path[rootlen:] + '/'*is_dir),
  888.                 referer=(2, mk_referer_param(referer)),
  889.                 text=(1, x + '/'*is_dir),
  890.                 mobile_desc=(0, mobile_desc),
  891.                 desktop_desc=(0, desktop_desc),
  892.             )
  893.         )
  894.     compressout.write_b('''            <!--</p>--></table>
  895.         </div></main>
  896.         
  897. <!-- INCLUDED FOOTER -->
  898. <footer><div id="footer">
  899.     <hr class="textonly"/>
  900.     <p>
  901.         Copyright © Oskar Skog<br/>
  902.         Website content released under the <a
  903.         href="https://creativecommons.org/licenses/by/4.0/" rel="license noopener"
  904.         target="_blank">Creative Commons Attribution (CC-BY 4.0)</a> license
  905.         and my software usually under the <span class="a"><a target="_blank"
  906.         rel="noopener"
  907.         href="https://opensource.org/license/BSD-2-Clause">FreeBSD license
  908.         (2-clause)</a>.</span>
  909.         <br/>
  910.         Images may be from other sites, I should have cited useful sources
  911.         somewhere on the page.
  912.         <span class="notprint">Contact me if I haven't.</span>
  913.     </p>
  914.     <p id="contact" class="notprint">
  915.         You can contact me at: <a href="mailto:oskar@oskog97.com"
  916.         rel="noopener" target="_blank">oskar@oskog97.com</a>
  917.         <span class="a">(<a href="/pgp-pub/oskar.asc"
  918.                             >PGP public key</a>)</span>
  919.     </p>
  920.     <p> <a class="notprint" href="https://oskog97.com/read/?path=/style.css">
  921.             CSS Stylesheet
  922.         </a>
  923.     </p>
  924. </div></footer>
  925. <!-- END OF INCLUDED FOOTER -->
  926.     </body>
  927. </html>\n''')
  928. def download(path):
  929.     if noindex(path):
  930.         compressout.write_h('X-Robots-Tag: noindex\n')
  931.     else:
  932.         compressout.write_h('X-Robots-Tag: index\n') # For verbosity.
  933.     try:
  934.         content = open(path).read()
  935.         if sys.version_info[0] == 2:
  936.             content.decode('utf-8')
  937.         compressout.write_h('Content-Type: text/plain; charset=UTF-8\n')
  938.         compressout.write_h(htmlescape.escape(
  939.                 'Link: <{}?path={}>',
  940.                 0, canonical_url,
  941.                 2, path[rootlen:]
  942.             ) + '; rel="canonical"; type="text/html"\n'
  943.         )
  944.     except:
  945.         compressout.write_h(htmlescape.escape(
  946.             'Link: <{}?path={}>; rel="canonical"\n',
  947.             0, canonical_url,
  948.             2, path[rootlen:]
  949.         )) # No type specified.
  950.     if if_none_match(path):
  951.         compressout.write_h('\n')
  952.         if os.getenv('REQUEST_METHOD') != 'HEAD':
  953.             compressout.write_b(content)
  954. def cat(path, referer):
  955.     '''
  956.     '''
  957.     def ol_content(text):
  958.         out_lines = []
  959.         ids = []
  960.         allowed_chars = string.ascii_letters + '_-'
  961.         for index, line in enumerate(text.split('\n')):
  962.             # Create a "permanent" fragment this line.
  963.             this_id = ''
  964.             # Find ids in Python and XHTML
  965.             for decltype in ('def', 'class'):
  966.                 if line.strip().startswith(decltype + ' ') and '(' in line:
  967.                     this_id = line.split(decltype, 1)[1].split('(')[0].strip()
  968.             if 'id="' in line:
  969.                 this_id = line.split('id="')[1].split('"')[0]
  970.             # Prevent bad ids.
  971.             for ch in this_id:
  972.                 if ch not in allowed_chars:
  973.                     this_id = ''
  974.                     break
  975.             if this_id in ids:
  976.                 this_id = ''
  977.             # Create the fragment identifier for the line.
  978.             if this_id:
  979.                 ids.append(this_id)
  980.                 idline = 'id="content_{}"'.format(this_id)
  981.             else:
  982.                 idline = ''
  983.             # Create line
  984.             out_lines.append(htmlescape.escape(
  985.                     '    <li id="{}"><pre translate="no" {}>{}</pre></li>\n',
  986.                     0, index + 1,
  987.                     0, idline,
  988.                     1, line,
  989.             ))
  990.         fragment_links = []
  991.         for fragment in sorted(ids):
  992.             fragment_links.append(
  993.                 (
  994.                     '<a class="quick" href="#content_{0}" translate="no"' +
  995.                     '>{0}</a>\n'
  996.                 ).format(
  997.                     fragment
  998.                 )
  999.             )
  1000.         return ''.join(out_lines), ''.join(fragment_links)
  1001.     
  1002.     try:
  1003.         content = open(path).read()
  1004.         if sys.version_info[0] == 2:
  1005.             content.decode('utf-8')
  1006.     except:
  1007.         if noindex(path):
  1008.             compressout.write_h('X-Robots-Tag: noindex\n')
  1009.         else:
  1010.             compressout.write_h('X-Robots-Tag: index\n')
  1011.         compressout.write_h('\n')
  1012.         compressout.write_b(content)
  1013.         return
  1014.     compressout.write_h(html_page)
  1015.     compressout.write_h('\n')
  1016.     if os.getenv('REQUEST_METHOD') == 'HEAD':
  1017.         return
  1018.     
  1019.     ignore, title, meta_description, p_description = mk_description(path)
  1020.     last_modified = time.strftime('%F', time.gmtime(os.stat(path).st_mtime))
  1021.     
  1022.     lines, fragment_links = ol_content(content)
  1023.     if not fragment_links:
  1024.         fragment_links = '(none)'
  1025.     
  1026.     compressout.write_b('''<!DOCTYPE html>
  1027. <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
  1028.     <head>
  1029.         <meta charset="utf-8"/>
  1030.         <meta name="viewport" content="width=device-width, initial-scale=1"/>
  1031.         <link rel="stylesheet" href="https://oskog97.com/style.css" type="text/css"/>
  1032.         <link rel="icon" type="image/png" href="/favicon.png"/>
  1033. <!-- End html5nc macro. -->
  1034. ''')
  1035.     compressout.write_b('''
  1036. <script type="application/ld+json">
  1037. {
  1038.     "@context":
  1039.     {
  1040.         "@vocab": "http://schema.org/"
  1041.     },
  1042.     "@type": "SoftwareSourceCode",
  1043.     "license": "https://opensource.org/licenses/BSD-2-Clause",
  1044.     "author":
  1045.     {
  1046.     ''')
  1047.     compressout.write_b('''
  1048.         "@type": "Person",
  1049.         "@id": "https://oskog97.com/",
  1050.         "name": "{0}",
  1051.         "url": "https://oskog97.com/"
  1052.     '''.format(owner))
  1053.     compressout.write_b('''
  1054.     },
  1055.     "publisher": {"@id": "https://oskog97.com/"},
  1056.     "copyrightHolder": {"@id": "https://oskog97.com/"},
  1057.     ''')
  1058.     compressout.write_b('''
  1059.     "url": "{}#code",
  1060.     "DateModified": "{}"
  1061.     '''.format(
  1062.         canonical_url + '?path=' + path[rootlen:],
  1063.         last_modified,
  1064.     ))
  1065.     compressout.write_b('''
  1066. }
  1067. </script>
  1068.     ''')
  1069.     parent_link = '/'.join(path.split('/')[:-1])[rootlen:]+'/'
  1070.     compressout.write_b(htmlescape.escape('''
  1071.         <link rel="stylesheet" type="text/css" href="/read/style.css"/>
  1072.         <title>{title}</title>
  1073.         <link rel="canonical" href="{canonical}"/>
  1074.         <link
  1075.             rel="alternate"
  1076.             href="{canonical}&amp;download=yes"
  1077.             type="text/plain"
  1078.         />
  1079.         <meta name="robots" content="{noindex_no}index"/>
  1080.         <meta name="description" content="{meta_description}"/>
  1081.     </head>
  1082.     <body>
  1083.         {navigation}
  1084. <main><div id="content">
  1085.     <h1 id="title" translate="no">{title}</h1>
  1086.     <div id="description">
  1087.         {content_description}
  1088.     </div>
  1089.     <table>
  1090.         <tr>
  1091.             <td>Last modified</td>
  1092.             <td><time datetime="{last_modified}">{last_modified}</time></td>
  1093.         </tr>
  1094.         <tr>
  1095.             <td>Lines</td>
  1096.             <td>{linecount}</td>
  1097.         </tr>
  1098.         {begin_debug}<tr>
  1099.             <td>Indexable</td>
  1100.             <td>{indexable}</td>
  1101.         </tr>{end_debug}
  1102.     </table>
  1103.     <p class="notprint read-nav">
  1104.         <a href="{my_url}?path={parent_dir}">Parent directory</a>
  1105.         <a href="{my_url}?path={path}&amp;download=yes" target="_blank">Download</a>
  1106.         <a href="{my_url}?sitemap=html">CGIread sitemap</a>
  1107.         <a href="{my_url}">Main page</a>
  1108.     </p>
  1109.     <p class="notprint">
  1110.         Quick links:\n{fragments}
  1111.     </p>
  1112. <ol id="code">
  1113. {content}
  1114. </ol>
  1115. </div></main>
  1116. ''',
  1117.         title=(2, title),
  1118.         content=(0, lines),
  1119.         parent_dir=(2, parent_link + mk_referer_param(referer)),
  1120.         navigation=(0, mk_navigation(referer, path[rootlen:])),
  1121.         canonical=(2, canonical_url + '?path=' + path[rootlen:]),
  1122.         path=(2, path[rootlen:]),
  1123.         noindex_no=(2, 'no' * noindex(path)),
  1124.         meta_description=(2, meta_description),
  1125.         content_description=(0, p_description),
  1126.         last_modified=(2, last_modified),
  1127.         linecount=(1, content.count('\n') + 1),
  1128.         indexable=(0, {True: 'No', False: 'Yes'}[noindex(path)]),
  1129.         fragments=(0, fragment_links),
  1130.         my_url=(0, my_url),
  1131.         begin_debug=(0,['<!--',''][compressout.debug_cookie]),
  1132.         end_debug=(0,['-->',''][compressout.debug_cookie]),
  1133.     ))
  1134.     compressout.write_b('''
  1135.         
  1136. <!-- INCLUDED FOOTER -->
  1137. <footer><div id="footer">
  1138.     <hr class="textonly"/>
  1139.     <p>
  1140.         Copyright © Oskar Skog<br/>
  1141.         Website content released under the <a
  1142.         href="https://creativecommons.org/licenses/by/4.0/" rel="license noopener"
  1143.         target="_blank">Creative Commons Attribution (CC-BY 4.0)</a> license
  1144.         and my software usually under the <span class="a"><a target="_blank"
  1145.         rel="noopener"
  1146.         href="https://opensource.org/license/BSD-2-Clause">FreeBSD license
  1147.         (2-clause)</a>.</span>
  1148.         <br/>
  1149.         Images may be from other sites, I should have cited useful sources
  1150.         somewhere on the page.
  1151.         <span class="notprint">Contact me if I haven't.</span>
  1152.     </p>
  1153.     <p id="contact" class="notprint">
  1154.         You can contact me at: <a href="mailto:oskar@oskog97.com"
  1155.         rel="noopener" target="_blank">oskar@oskog97.com</a>
  1156.         <span class="a">(<a href="/pgp-pub/oskar.asc"
  1157.                             >PGP public key</a>)</span>
  1158.     </p>
  1159.     <p> <a class="notprint" href="https://oskog97.com/read/?path=/style.css">
  1160.             CSS Stylesheet
  1161.         </a>
  1162.     </p>
  1163. </div></footer>
  1164. <!-- END OF INCLUDED FOOTER -->
  1165.     </body>
  1166. </html>
  1167. ''')
  1168. def if_none_match(path):
  1169.     '''
  1170.     ETag handling for `cat`, `ls` and `download`:
  1171.     
  1172.     
  1173.     Returns `True` if content needs to be generated.
  1174.     Outputs necessary headers and 304 statuses.
  1175.     '''
  1176.     try:
  1177.         meta_time = os.stat(path + '.info').st_mtime
  1178.     except:
  1179.         meta_time = 0
  1180.     if sys.version_info[0] > 2:
  1181.         query_string = os.getenv('QUERY_STRING', '').encode('utf-8')
  1182.     else:
  1183.         query_string = os.getenv('QUERY_STRING', '')
  1184.     ETag = '"{}{}-{}({})-{}-({}-{})"'.format(
  1185.         'x'*('application/xhtml+xml' in html_page),
  1186.         'z'*('gzip' in os.getenv('HTTP_ACCEPT_ENCODING', '')),
  1187.         os.stat(path).st_mtime,
  1188.         meta_time,
  1189.         base64.b64encode(query_string),
  1190.         os.stat('index.py').st_mtime,
  1191.         os.stat('read.cfg').st_mtime,
  1192.     )
  1193.     compressout.write_h('Vary: If-None-Match\n')
  1194.     compressout.write_h('ETag: {}\n'.format(ETag))
  1195.     compressout.write_h(
  1196. '''X-ETag-Synopsis: [x][z]-<f_time>(<m_time>)-<query>-(<s_time>-<c_time>)
  1197. X-ETag-Description-x: "Client accepts application/xhtml+xml"
  1198. X-ETag-Description-z: "Content-Encoding: gzip"
  1199. X-ETag-Description-f_time: "Unix last modified time for the requested file"
  1200. X-ETag-Description-m_time: "Unix last modified time for the file's metadata"
  1201. X-ETag-Description-query: "base64 encoded $QUERY_STRING"
  1202. X-ETag-Description-s_time: "Unix last modified time for '/read/index.py'"
  1203. X-ETag-Description-c_time: "Unix last modified time for '/read/read.cfg'"
  1204. ''')
  1205.     if os.getenv('HTTP_IF_NONE_MATCH', '') == ETag:
  1206.         compressout.write_h('Status: 304\n\n')
  1207.         return False
  1208.     else:
  1209.         return True
  1210. def is_injection_attempt(path_param, referer_URI, referer_title):
  1211.     '''
  1212.     Various checks to see if any form of injection attempt has been
  1213.     made.  This function checks the `path`, `referer` and `title`
  1214.     parameters.
  1215.     
  1216.     Returns True if the request is an injection attempt.
  1217.     
  1218.     - XSS
  1219.     - URL injection
  1220.     - Spam injection
  1221.     - Restricted files access
  1222.     '''
  1223.     # If the path parameter contains an XSS attempt, it can't be corrected
  1224.     evil = False
  1225.     # Prevent attacks.
  1226.     if '..' in path_param:
  1227.         return True
  1228.     for var in referer_URI, referer_title:
  1229.         for ch in var:
  1230.             if ord(ch) < 32:
  1231.                 return True
  1232.             if ch in '<>&\'"':
  1233.                 return True
  1234.             # NOTICE: The following will limit parameters to ASCII.
  1235.             if ord(ch) > 126:
  1236.                 return True
  1237.     # Prevent linking to Mallory.
  1238.     for start in ('http://', 'https://', '//', 'ftp://'):
  1239.         if referer_URI.startswith(start):
  1240.             hostname = referer_URI.split('//')[1].split('/')[0]
  1241.             if hostname not in conf['allowed-referer-hosts']:
  1242.                 return True
  1243.             else:
  1244.                 break
  1245.     else:
  1246.         if ':' in referer_URI:
  1247.             return True
  1248.     # Prevent injected spam
  1249.     if spammy.spammy(referer_title) or len(referer_title) > 42:
  1250.         return True
  1251.     # No match.
  1252.     return False
  1253. def handle_injection_attempt(path_param, referer_URI, referer_title):
  1254.     '''
  1255.     Decide if the injection attempt was due to innocently following
  1256.     a malicious link or due to creating one.
  1257.     '''
  1258.     # Check if the URL can be sanitized.
  1259.     if is_injection_attempt(path_param, '', ''):
  1260.         destination = 'https://en.wikipedia.org/wiki/Data_validation'
  1261.     else:
  1262.         destination = my_url + '?path=' + path_param
  1263.     redirect_spam(destination)
  1264. def main():
  1265.     '''
  1266.     `compressout.init` MUST be called before `main`
  1267.     and `compressout.done` after.
  1268.     '''
  1269.     # HTML vs XHTML
  1270.     global html_page
  1271.     html_page = 'Vary: Accept\n'
  1272.     if 'application/xhtml+xml' in os.getenv('HTTP_ACCEPT', ''):
  1273.         html_page += 'Content-Type: application/xhtml+xml; charset=UTF-8\n'
  1274.     else:
  1275.         html_page += 'Content-Type: text/html; charset=UTF-8\n'
  1276.     # Check that the method is either GET, HEAD or OPTIONS.
  1277.     if os.getenv('REQUEST_METHOD') not in ('GET', 'HEAD'):
  1278.         if os.getenv('REQUEST_METHOD') != 'OPTIONS':
  1279.             compressout.write_h('Status: 405\n')
  1280.         compressout.write_h('Allow: GET, HEAD, OPTIONS\n')
  1281.         compressout.write_h('Content-Type: text/plain\n')
  1282.         compressout.write_h('\n')
  1283.         if os.getenv('REQUEST_METHOD') != 'OPTIONS':
  1284.             compressout.write_b('Method not allowed!\n')
  1285.         compressout.write_b('Allowed methods: GET, HEAD, OPTIONS\n')
  1286.         return
  1287.     # Get the parameters.
  1288.     params = cgi.FieldStorage()
  1289.     path = path_param = params.getfirst('path', default='')
  1290.     referer_URI = params.getfirst('referer', default='')
  1291.     referer_title = params.getfirst('title', default='Back')
  1292.     referer = (referer_URI, referer_title)
  1293.     download_flag = params.getfirst('download', default='no')
  1294.     sitemap_param = params.getfirst('sitemap', default='none')
  1295.     
  1296.     if not os.getenv('QUERY_STRING'):
  1297.         index_page()
  1298.         return
  1299.         
  1300.     # Bad request, but will match the evil patterns.
  1301.     # Keep it before the evil stopper.
  1302.     if bool(path_param) and not path_param.startswith('/'):
  1303.         status400('`path` is not relative to this site. (No leading slash.)')
  1304.         return
  1305.     
  1306.     # Do not allow evil requests.
  1307.     allow = True
  1308.     # Keep things within the server root.
  1309.     try:
  1310.         path = os.path.realpath(root + path)
  1311.     except:
  1312.         allow = False
  1313.     if path != root and not path.startswith(root + '/'):
  1314.         allow = False
  1315.     # Stop at forbidden paths. #1/2
  1316.     for regex in conf['noaccess']:
  1317.         if re.match(regex, path[rootlen:]) is not None:
  1318.             allow = False
  1319.     
  1320.     # Prevent XSS, URL injection, spam injection and miscellaneous assholery.
  1321.     if is_injection_attempt(path_param, referer_URI, referer_title):
  1322.         allow = False
  1323.     if not allow:
  1324.         handle_injection_attempt(path_param, referer_URI, referer_title)
  1325.         return
  1326.     
  1327.     # Bad requests:
  1328.     if download_flag not in ('yes', 'no'):
  1329.         status400('`download` MUST be "yes", "no" or unset.')
  1330.         return
  1331.     if bool(path_param) and sitemap_param != 'none':
  1332.         status400('The `sitemap` parameter cannot be used with any other.')
  1333.         return
  1334.     if download_flag == 'yes' and bool(referer_URI):
  1335.         status400("`download=yes` can't be used with the `referer` parameter.")
  1336.         return
  1337.     if sitemap_param not in ('none', 'xml', 'html'):
  1338.         status400('`sitemap` MUST be "html", "xml" or unset.')
  1339.         return
  1340.     if download_flag == 'yes' and not bool(path_param):
  1341.         status400('Nothing to `download`. Use the `path` parameter.')
  1342.         return
  1343.     if bool(referer_URI) and not bool(path_param):
  1344.         status400('`referer` cannot be used without `path`')
  1345.         return
  1346.     if referer_title != 'Back' and not bool(referer_URI):
  1347.         status400('`referer` is not set.')
  1348.         return
  1349.     
  1350.     if allow:
  1351.     # Generate sitemap?
  1352.         if sitemap_param != 'none':
  1353.             sitemap(sitemap_param)
  1354.         else:
  1355.             # Stop at forbidden paths. #2/2
  1356.             for regex in conf['topsecret']:
  1357.                 if re.match(regex, path[rootlen:]) is not None:
  1358.                     status404()
  1359.                     break
  1360.             else:
  1361.                 # Allowed to be seen.
  1362.                 try:
  1363.                     os.listdir(path)
  1364.                     if download_flag == 'no':
  1365.                         if if_none_match(path):
  1366.                             ls(path, referer)
  1367.                     else:
  1368.                         status400("Can't download a directory.")
  1369.                 except OSError as e:
  1370.                     if e.errno == errno.ENOTDIR:
  1371.                         if download_flag == 'no':
  1372.                             if if_none_match(path):
  1373.                                 cat(path, referer)
  1374.                         else:
  1375.                             # `download` sets a few headers.
  1376.                             download(path)
  1377.                     elif e.errno == errno.ENOENT:
  1378.                         status404()
  1379.                     else:
  1380.                         raise ValueError(
  1381.                             'errno must be either ENOTDIR or ENOENT'
  1382.                         )
  1383. if __name__ == '__main__':
  1384.     compressout.init()
  1385.     main()
  1386.     compressout.done()