CGI gzip compression module
Simple CGI output module. Uses gzip compression on the output stream if the client accepts it.
Last modified | |
Lines | 280 |
Parent directory Download CGIread sitemap Main page
Quick links: done init output overload_test write_b write_body write_h write_head
#!/usr/bin/python
import os
import sys
import subprocess
# 2020-09-17 Somehow broke gzip compression
# Can't get subprocess.Popen to work anymore
# 2022-**-** Increased max load average from 3.5 to 6
# 2022-05-21 and to 8
# 2022-08-08 to 12
'''
compressout
===========
Simple CGI output compression module.
Not all webservers support compressing the output stream of CGI scripts.
This script determines whether or not a client accept gzip compression and
compresses the output stream of the script.
NOTICE: The `cgitb` module will write to stdout if the script crashes,
you should use a browser that does not accept gzip, when you are
testing your scripts.
NOTICE: This module uses two global variable: `use_gzip` and `body`.
It's supposed to be used like an object, but rather than using a class,
this is imported "pre-created" or so to say.
NOTICE: There are two constants:
`max_load_avg_1min`:
This is the maximum allowed load average under one minute.
If the one minute load average exceeds this value, compressout will
return a 503 response to the client and abort the process.
`http503_body`:
A plain text error message to send if the server is overloaded.
You should modify these constants to fit your own needs.
Example / TL;DR
===============
import compressout
compressout.init()
compressout.write_head('Content-Type: text/plain\r\n')
compressout.write_head('Foo: test\r\nBar: test\r\n')
# Blank line required for terminating the HEAD section
compressout.write_head('\r\n')
compressout.write_body('Hello world!\n')
compressout.write_body('Bye.\n')
compressout.done()
Functions
=========
init(write_headers=True)
------------------------
Initialize the module. This function will detect if the client
supports gzip.
If `write_headers` is True, the function writes a
'Vary: Content-Encoding' header and (if gzip is used) a
'Content-Encdoing: gzip' header.
write_head(s) and write_h(s)
----------------------------
This function is used to print all HTTP headers **and the blank line
separating the head from the body**.
Write `s` to standard output, will never go through gzip.
write_body(s) and write_b(s)
----------------------------
Write part of body.
NOTICE: You need to have printed the blank line after the headers
with the `write_h` (or `write_head`) fuction.
If gzip is supported by the client
----------------------------------
`s` will be appended to a local buffer which the `done` function
will compress and print.
If gzip is not supported
------------------------
`s` will go straight to stdout. The `done` function won't do
anything.
done()
------
Done writing output.
This function will invoke gzip.
Dos and don'ts
==============
* Try to call `init` and `done` at convenient locations like on the
"outside" of a main function, i.e. don't repeat yourself by calling
these two functions everywhere in your code.
* Never call `write_head` after any call to `write_body`.
* Always call `done` when your done.
* Use only compressout to write output, otherwise you'll have a mess.
* NOTICE: The `cgitb` module will write to stdout if the script
crashes, you should use a browser that does not accept gzip,
when you are testing your scripts. Eg, lwp-request.
`GET http://example.com/ | less` is excellent for debuggin.
'''
### GLOBALS ###
use_gzip = None # Whether or not to compress the body
body = '' # The body is stored here if it is to be compressed
### END GLOBALS ###
### CONSTANTS -- Configure for your own needs ###
# If the load average of the last one minute exceeds the hard coded value,
# this script will return a 503 response and abort the process.
max_load_avg_1min = 12 # 3.5
http503_body = '''
Service temporarily unavailable!
Wait at least two minutes before trying again.
Re-attempting prematurely may result in banning your IP address.
-- END --
'''
# #############################################
if sys.version_info[0] > 2:
def output(s):
if isinstance(s, str):
sys.stdout.buffer.write(s.encode('utf-8'))
elif isinstance(s, bytes):
sys.stdout.buffer.write(s)
else:
raise TypeError("Unsupported datatype")
flush = sys.stdout.buffer.flush
else:
output = sys.stdout.write
flush = sys.stdout.flush
def overload_test(too_late=False):
'''
'''
if os.getloadavg()[0] > max_load_avg_1min:
if not too_late:
output('Status: 503\n')
output('Content-Type: text/plain\n')
output('Retry-After: 90\n')
output(http503_body)
flush()
#os.abort()
sys.exit(1)
def init(write_headers=True):
'''
Initialize the module. This function will detect if the client
support gzip. This will also set the global variable `debug_cookie`.
If `write_headers`, write a 'Vary' and (if used)
'Content-Encoding' header.
'''
global use_gzip
global body
global debug_cookie
# This is the only place where sending a 503 message will work.
# write_h:
# - Message body may need to be compressed.
# - Possibility of conflicting Status headers.
# write_b:
# - Message body may need to be compressed.
# - Message body may be application/xhtml+xml
# done:
# - Message body needs to be compressed if `use_gzip`.
# - Body has already been written if not `use_gzip`.
overload_test(too_late=False)
use_gzip = 'gzip' in os.getenv('HTTP_ACCEPT_ENCODING', '')
body = ''
use_gzip = False
if write_headers:
output('Vary: Accept-Encoding\n')
if use_gzip:
output('Content-Encoding: gzip\n')
debug_cookie = 'debug=on' in os.getenv('HTTP_COOKIE', '')
if 'debug=on' in os.getenv('QUERY_STRING', ''):
output('Set-Cookie: debug=on\n')
debug_cookie = True
if 'debug=off' in os.getenv('QUERY_STRING', ''):
output('Set-Cookie: debug=off\n')
debug_cookie = False
def write_head(s):
write_h(s)
def write_h(s):
'''
Write part of header.
Write `s` to standard output, will never go through gzip.
'''
overload_test(too_late=True)
output(s)
def write_body(s):
write_b(s)
def write_b(s):
'''
Write part of body.
gzip is supported by the client
-------------------------------
`s` will be appended to a local buffer
which `done` will compress and print.
gzip is not supported
---------------------
`s` will go straight to stdout.
'''
global body
overload_test(too_late=True)
if use_gzip:
body += s
else:
output(s)
def done():
'''
Done writing output.
This function will invoke gzip.
'''
overload_test(too_late=True)
if use_gzip:
gzip = subprocess.Popen(
['gzip'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
if sys.version_info[0] > 2:
body = body.encode('utf-8')
sys.stderr.write('Body encoded\n')
sys.stderr.write('Just before communicate\n')
gzip_stdout = gzip.communicate(body)[0]
sys.stderr.write('Just after communicate\n')
#gzip_stdout = things[0]
#sys.stderr.write('After extracting data\n')
#sys.stderr.write(gzip_stderr)
output(gzip_stdout)
sys.stderr.write('done() complete\n')