summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAssaf Gordon <assafgordon@gmail.com>2016-05-04 21:58:17 (GMT)
committerAssaf Gordon <assafgordon@gmail.com>2016-05-05 04:03:13 (GMT)
commitda14329059f007b814d124a9d339ce1ec9d506f0 (patch)
treeaaa4caa442cf4033af830e1a31531417bca3b679
parentef8f00114306ce34c2d27f5d74f4a0a29ce83e90 (diff)
downloadcgi-tools-da14329059f007b814d124a9d339ce1ec9d506f0.zip
cgi-tools-da14329059f007b814d124a9d339ce1ec9d506f0.tar.gz
cgi-tools-da14329059f007b814d124a9d339ce1ec9d506f0.tar.bz2
Import cgi_tools package source code files
-rw-r--r--TODO.md4
-rw-r--r--cgi_tools/__init__.py9
-rw-r--r--cgi_tools/http_responses.py55
-rw-r--r--cgi_tools/params.py31
-rw-r--r--cgi_tools/system.py121
-rw-r--r--cgi_tools/validators.py39
6 files changed, 258 insertions, 1 deletions
diff --git a/TODO.md b/TODO.md
index bb4582a..dc0dd60 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,5 +2,7 @@ TODO List
=========
* Get version from git (but also package version in dist)
-* Documentations
+* Documentation
+ * Describe scripts
+ * Docstring exported functions (https://www.python.org/dev/peps/pep-0257/)
* version 2: use pythonian idioms \ No newline at end of file
diff --git a/cgi_tools/__init__.py b/cgi_tools/__init__.py
new file mode 100644
index 0000000..a921ae2
--- /dev/null
+++ b/cgi_tools/__init__.py
@@ -0,0 +1,9 @@
+from .system import force_C_locale, set_resource_limits, \
+ run_cmd_list, check_run_cmd_list
+
+from .http_responses import http_bad_request_error, http_server_error, \
+ http_error, log
+
+from .validators import valid_regex, valid_int, valid_float
+
+from .params import save_cgi_file_param
diff --git a/cgi_tools/http_responses.py b/cgi_tools/http_responses.py
new file mode 100644
index 0000000..affb78c
--- /dev/null
+++ b/cgi_tools/http_responses.py
@@ -0,0 +1,55 @@
+from __future__ import print_function
+import sys, random
+
+def generate_request_code():
+ return 512 + random.uniform(0,1)
+
+req_code = generate_request_code()
+
+def log(*args):
+ """ Write MSG to STDERR """
+ prefix = "request %s: " % str(req_code)
+ msg = prefix + ' '.join(map(str, args))
+ #TODO: protect against non ASCII output
+ # (also against invalid UTF-8)
+ print (msg, file=sys.stderr)
+
+
+def log_req_info():
+ # TODO: print request information (remote_addr, query_string)
+ pass
+
+def http_error(http_code,http_status,msg):
+ """
+ Send an error code + status to the HTTP server,
+ and log the error on STDERR as well,
+ then terminate the script with exit-code 1.
+
+ http_code: int ( 40X = client errors, 50X = server errors)
+ http_status: text message
+ msg: any text
+
+ Message is always sent as plain-text content.
+ """
+ log ("returned HTTP error %d (%s): %s" % (http_code, http_status, msg))
+ print ("Status: %d %s" % (http_code, http_status))
+ print ("Content-Type: text/plain")
+ print ("")
+ print (msg)
+ sys.exit(1)
+
+
+
+def http_bad_request_error(msg):
+ """
+ Shortcut for sending client errors (HTTP 400/Bad Request)
+ """
+ http_error(400,"Bad Request",msg)
+
+
+
+def http_server_error(msg):
+ """
+ Shortcut for sending server errors (HTTP 500)
+ """
+ http_error(500,"Internal Server Error",msg)
diff --git a/cgi_tools/params.py b/cgi_tools/params.py
new file mode 100644
index 0000000..397b2d3
--- /dev/null
+++ b/cgi_tools/params.py
@@ -0,0 +1,31 @@
+from tempfile import NamedTemporaryFile
+from .http_responses import http_bad_request_error, http_server_error
+
+def save_cgi_file_param(form,var_name,suffix=None):
+ if not var_name in form:
+ msg = "missing CGI file parameter '%s'" % (var_name)
+ http_bad_request_error(msg)
+
+ f = form[var_name]
+ if not f.file:
+ msg = "invalid '%s' parameter (expecting file-upload)" % (var_name)
+ http_server_error(msg)
+
+ local_fn=""
+ remote_fn=f.filename
+ try:
+ if not suffix:
+ suffix=""
+ outf = NamedTemporaryFile(suffix=suffix,delete=False)
+ local_fn = outf.name
+ while 1:
+ chunk = f.file.read(100000)
+ if not chunk: break
+ outf.write (chunk)
+ outf.close()
+
+ return (local_fn,remote_fn)
+ except IOError as e:
+ http_server_error("cgi file upload I/O error: %s" % (str(e)))
+ except OSError as e:
+ http_server_error("cgi file upload OS error: %s" % (str(e)))
diff --git a/cgi_tools/system.py b/cgi_tools/system.py
new file mode 100644
index 0000000..7ee20f7
--- /dev/null
+++ b/cgi_tools/system.py
@@ -0,0 +1,121 @@
+from __future__ import print_function
+import sys, os, cgi, re, locale, resource, time, signal
+from subprocess import Popen, PIPE
+
+from .http_responses import http_server_error, log
+
+def alarm_signal_handler(signum, frame):
+ http_server_error("server error: wall-time limit reached")
+
+def xcpu_signal_handler(signum, frame):
+ http_server_error("server error: cpu-time limit reached")
+
+def xfsz_signal_handler(signum, frame):
+ http_server_error("server error: file-size limit reached")
+
+
+def set_resource_limits(walltime=3,cputime=1,filesize=10000,
+ nice=10):
+ """
+ Self-imposed limits - reduce risk of abuse.
+ Adjust values as needed (per script/scenario)
+
+ walltime = max. seconds of walltime
+ cputime = max. seconds of cputime (not wall time)
+ filesize = in bytes, per-process
+ nice = nice level
+ """
+
+ max_nofile = 30 # num. of open descriptors, per process
+ max_data = 50000000 # in bytes, per-process (when using brk/sbrk)
+ max_vmem = 100000000 # in bytes, per-process (when mmap)
+
+ try:
+ signal.signal(signal.SIGALRM, alarm_signal_handler)
+ signal.signal(signal.SIGXFSZ, xfsz_signal_handler)
+ signal.signal(signal.SIGXCPU, xcpu_signal_handler)
+
+ # Allow +1 for hard-limit - thus this script will receive
+ # SIGXCPU/SIGXFSZ and will terminate cleanly (with HTTP error
+ # returned)
+ resource.setrlimit(resource.RLIMIT_CPU, (cputime,cputime+1))
+ resource.setrlimit(resource.RLIMIT_FSIZE, (filesize,filesize*2))
+ resource.setrlimit(resource.RLIMIT_NOFILE, (max_nofile,max_nofile+1))
+ resource.setrlimit(resource.RLIMIT_DATA, (max_data,max_data))
+
+ # Max. wall time limit. SIGALRM will be triggered if expired.
+ signal.alarm(walltime)
+
+ # use AS as alternative to VMEM if the attribute isn't defined.
+ # http://stackoverflow.com/a/30269998/5731870
+ # http://git.savannah.gnu.org/cgit/bash.git/tree/builtins/ulimit.def#n154
+ if hasattr(resource,'RLIMIT_VMEM'):
+ resource.setrlimit(resource.RLIMIT_VMEM,(max_vmem,max_vmem))
+ elif hasattr(resource,'RLIMIT_AS'):
+ resource.setrlimit(resource.RLIMIT_AS, (max_vmem,max_vmem))
+
+ os.nice(nice)
+ except resource.error as e:
+ http_server_error("failed to set resource limits (%s)" % (str(e)))
+ except OSError as e:
+ http_server_error("failed to renice (%s)" % (str(e)))
+
+
+def force_C_locale():
+ """
+ Force C/POSIX locale for this script, and all executed children.
+ """
+ locale.setlocale(locale.LC_ALL,'C')
+ os.environ["LC_ALL"] = 'C'
+ os.environ["LC_LANG"] = 'C'
+ os.environ["LANG"] = 'C'
+ os.environ["LANGUAGE"] = 'C'
+ os.environ["LC_CTYPE"] = 'C'
+
+ #TODO: consider this (with ignoring non-ascii output)
+ #sys.stdout = codecs.getwriter('ascii')(sys.stdout)
+
+
+
+
+def is_string(arg):
+ # TODO:
+ # for Python 3: isinstance(arg, str)
+ return isinstance(arg, basestring)
+
+
+def run_cmd_list(cmd):
+ # A single string parameter: convert to list
+ if is_string(cmd):
+ cmd = [ cmd ]
+
+ msg = ' '.join(cmd)
+ log ("executing: " + msg)
+
+ try:
+ devnull = open('/dev/null','r')
+ p = Popen(cmd,shell=False,stdin=devnull,stdout=PIPE,stderr=PIPE)
+ devnull.close()
+ (out,err) = p.communicate()
+ ## NOTE:
+ ## returned values are not necessarily ASCII, or even valid text/utf/etc.
+ ## ALWAYS sanitize output.
+ ## Python (at least v2) has severe problems handling non-ascii
+ ## characters without extra processing.
+ return ( p.returncode==0, p.returncode, out, err )
+ except IOError as e:
+ http_server_error("failed to execute '%s': IOError: %s" % (str(cmd[0]), str(e)))
+ except OSError as e:
+ http_server_error("failed to execute '%s': %s" % (str(cmd[0]), str(e)))
+
+
+def check_run_cmd_list(cmd):
+ # A single string parameter: convert to list
+ if is_string(cmd):
+ cmd = [ cmd ]
+
+ (ok,exitcode,out,err) = run_cmd_list(cmd)
+ if not ok:
+ http_server_error("command '%s' returned error (exit code %d)" % \
+ (cmd[0],exitcode))
+ return (out,err)
diff --git a/cgi_tools/validators.py b/cgi_tools/validators.py
new file mode 100644
index 0000000..cd4610d
--- /dev/null
+++ b/cgi_tools/validators.py
@@ -0,0 +1,39 @@
+import re
+from .http_responses import http_server_error
+
+def valid_regex(regex,value):
+ """
+ """
+ if not value:
+ return False
+ if len(str(value).strip())==0:
+ return False
+ t = re.compile("^" + regex +"$")
+ if not t.match(value):
+ return False
+ return True
+
+
+def valid_int(i):
+ """
+ """
+ if not i:
+ return False;
+ try:
+ i = int(i)
+ return True
+ except ValueError as e:
+ return False;
+
+
+def valid_float(i):
+ """
+ """
+ if not i:
+ return False;
+ try:
+ i = float(i)
+ return True
+ except ValueError as e:
+ return False;
+