httpd-serve is a Python HTTP server, which uses a dedicated Emacs
process to serve Wiki pages directly. It supports editing of pages,
dynamic search queries (if glimpse is installed), etc.
This new version supports gzip'd .html files (i.e., a reference to
foo.html will serve either foo.html or foo.html.gz). It also fixes
several bugs, and adds a nasty LIMITATIONS section at the beginning of
the script file.
This server has been working stably on alice.dynodns.net for several
months, and is capable of serving around 100 pages/sec. Be sure to
read the directions both in this script, and in emacs-wiki. Send mail
to me if you have troubles getting it setup. Maybe we can work out a
HOWTO together... :)
John Wiegley <jo...@gnu.org>
----------------------------------------------------------------------
#!/usr/bin/env python
# $Revision: 1.27 $
#
# A simple HTTP server, written in Python, that can be used to serve
# Emacs Wiki pages directly from a running Emacs process.
#
# Usage is simple:
#
# httpd-serve --port 8080 --load ~/Emacs/startup.el /var/www
#
# This will start a dedicated Emacs session, and will listen on port
# 8080 for HTTP requests. If the request is for a plain file, it will
# be served directly from /var/www -- without using Emacs. If the
# request is not for a plain file, it is passed to Emacs' httpd.el.
#
# startup.el should contain any startup routines you wish to load into
# your Emacs web server.
#
# NOTE: This script has only been test on Debian GNU/Linux, using
# gnuserv 2.1alpha (with Unix domain sockets), and Python's pty
# module. See my web page for updates or new versions:
#
# http://www.gci-net.com/users/j/johnw/emacs.html
#
# LIMITATIONS: The only flaw known so far is that httpd-serve must
# have access to an X server. This is not necessary on all systems,
# and sometimes a plain pty works just fine. You can try setting
# no_wins=1 at the top of this file. If that works, and you're able
# to serve pages, you should not need to run httpd-serve under X.
#
# John Wiegley <jo...@gnu.org>
import os
import pty
import pwd
import re
import sys
import signal
import string
import time
import gzip
from socket import *
from signal import *
from resource import *
import cStringIO
import SimpleHTTPServer
import BaseHTTPServer
# Changing these numbers will affect the memory limits of the Emacs
# dedicated process. These numbers are in megabytes.
MAX_HEAP = 64
MAX_STACK = 2
port = 80
log_file = None
pid_file = None
startup = "_unknown"
debug = 0
daemon = 0
user = os.geteuid()
no_wins = 0
ignore_urls = [
# block the Nimbda worm
"^/(MSADC|msadc|_mem_bin|_vti_bin|scripts)/",
"(\.(exe|dll)|c\+dir)",
]
class HTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
"This class serves possibly httpd.el-related HTTP requests."
extensions_map = {
'': 'text/plain', # Default, *must* be present
'.css': 'text/css',
'.html': 'text/html',
'.htm': 'text/html',
'.gif': 'image/gif',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
}
time_format = "%a, %d %b %Y %H:%M:%S %Z"
def parse_request(self):
"""Parse a request (internal). This is identical to
BaseHTTPServer.parse_request, except that it leaves self.rfile
alone."""
self.request_version = version = "HTTP/0.9" # Default
requestline = self.raw_requestline
if requestline[-2:] == '\r\n':
requestline = requestline[:-2]
elif requestline[-1:] == '\n':
requestline = requestline[:-1]
self.requestline = requestline
words = string.split(requestline)
if len(words) == 3:
[command, path, version] = words
if version[:5] != 'HTTP/':
self.send_error(400, "Bad request version (%s)" % `version`)
return 0
elif len(words) == 2:
[command, path] = words
if command != 'GET':
self.send_error(400,
"Bad HTTP/0.9 request type (%s)" % `command`)
return 0
elif not requestline:
line = ""
try: line = self.rfile.readline()
except: pass
if not line:
return 0
else:
self.raw_requestline = line
return self.parse_request()
else:
self.send_error(400, "Bad request syntax (%s)" % `requestline`)
return 0
self.command, self.path, self.request_version = command, path, version
return 1
def handle(self):
"""Handle an HTTP request.
If it's a plain file request, handle it in Python. Otherwise,
turn it over to httpd.el."""
try:
self.raw_requestline = line = self.rfile.readline()
if not self.parse_request():
return
for regexp in ignore_urls:
if re.search(regexp, self.path):
return
except:
return
content_length = 0
modified_since = 0
accept_gzip = 0
encoding = 0
data = []
while line != "\r\n" and line != "\n":
if line[-2:] == '\r\n':
line = line[:-2]
elif line[-1:] == '\n':
line = line[:-1]
match = re.match("^Content-[Ll]ength:\s+([0-9]+)", line)
if match:
content_length = int(match.group(1))
else:
match = re.match("^Accept-[Ee]ncoding:\s+((x-)?gzip)", line)
if match:
accept_gzip = match.group(1)
else:
match = re.match("^If-Modified-Since:\s+(.+) GMT", line)
if match:
modified_since = time.strptime(match.group(1),
self.time_format)
modified_since = time.mktime(modified_since)
data.append(line)
line = self.rfile.readline()
data.append(line)
data = re.sub("\"", "\\\"", string.join(data, '\n'))
content = ""
if content_length > 0:
content = self.rfile.read(content_length)
content = re.sub("\"", "\\\"", content)
# After translating the path, see whether httpd-serve or Emacs
# will be the one to handle the request
self.translated_path = self.translate_path(self.path)
if self.command in ("GET", "HEAD") and \
(os.path.isfile(self.translated_path) or \
os.path.isfile(self.translated_path + ".gz")):
if os.path.isfile(self.translated_path):
real_file = self.translated_path
else:
real_file = self.translated_path + ".gz"
info = os.stat(real_file)
mtime = info[8]
if modified_since and mtime <= modified_since:
self.send_response(304, "Not modified")
self.log_request(304)
return
revision = "$Revision: 1.27 $"
match = re.match(": ([0-9.]+) \$", revision)
if match:
revision = match.group(1)
else:
revision = "0.1"
code = 200
type = self.guess_type(self.translated_path)
if re.search("\.gz$", real_file):
encoding = "x-gzip"
headers = [
"HTTP/1.0 200 OK",
"Server: httpd-serve/%s" % revision,
"MIME-Version: 1.0",
"Date: %s" % self.date_time_string(),
"Last-Modified: %s" %
time.strftime(self.time_format, time.localtime(mtime)),
"Content-Type: %s" % type
]
if encoding:
headers.append("Content-Encoding: %s" % encoding)
headers.append("Content-Length: %d" % info[6])
fd = open(real_file, "rb")
data = fd.read()
fd.close()
# jww (2001-05-21): Because many browsers are still
# braindead, compression is only safe with html files.
if type != "text/html":
accept_gzip = 0
else:
info = None
# If Emacs died while we were waiting for a web request,
# restart it now. In theory this should never occur, but
# Emacs has been known to core silently.
try:
status = os.waitpid(emacs_pid, os.WNOHANG)
if status != (0, 0):
self.log_message("Emacs died waiting with status %s",
`status`)
restart_emacs(self)
except:
self.log_message("Failed to communicate with Emacs process")
restart_emacs(self)
file = gnuserv_socket()
if not file:
self.send_error(501, "Gnuserv not found; please try again")
restart_emacs(self)
return
try:
alarm(60)
s = socket(AF_UNIX, SOCK_STREAM)
s.connect(file)
s.send("""(server-eval
'(with-output-to-string
(httpd-serve "%s" "%s")))\004""" %
(data, content))
s.shutdown(1)
alarm(0)
except AlarmTimeout:
self.send_error(501, "Timeout waiting for Emacs eval")
restart_emacs(self)
return
data = s.recv(300)
if data == "nil":
self.send_error(501, "Error handling request")
return
elif not data:
self.send_error(501, "Something bad happened to Emacs")
restart_emacs(self)
return
code = 501
if data:
l = string.split(data)
if len(l) > 1:
code = l[1]
f = cStringIO.StringIO()
while data:
f.write(data)
data = s.recv(300)
s.close()
data = f.getvalue()
f.close()
pos = string.find(data, '\n\n')
if pos < 0: return
headers = string.split(data[: pos], '\n')
data = data[pos :]
if modified_since:
mtime = 0
for header in headers:
match = re.match("^Last-Modified:\s+(.+)", header)
if match:
mtime = time.strptime(match.group(1), self.time_format)
break
if mtime <= modified_since:
self.send_response(304, "Not modified")
self.log_request(304)
return
if accept_gzip and not encoding:
f = cStringIO.StringIO()
g = gzip.GzipFile(filename = "", mode = "wb", fileobj = f)
g.write(data)
g.close()
data = f.getvalue()
i = len(headers) - 1
while i >= 0:
if re.match("^Content-Length:", headers[i]):
headers[i] = "Content-Length: %d" % len(data)
break
i = i - 1
headers = headers[:-1] + \
["Content-Encoding: %s" % accept_gzip] + \
headers[-1:]
elif not info:
# Add in the two extra bytes for the conversion from \n to
# \r\n applied by this server
i = len(headers) - 1
while i >= 0:
match = re.match("^Content-Length:\s+([0-9]+)", headers[i])
if match:
headers[i] = "Content-Length: %d" % \
(int(match.group(1)) + 2)
break
i = i - 1
try:
self.wfile.write(string.join(headers, '\r\n'))
self.wfile.write('\r\n')
self.wfile.write('\r\n')
if self.command != "HEAD":
self.wfile.write(data)
except:
self.log_message("Error sending HTTP data to client")
return
self.log_request(code, len(data))
def send_head(self):
"""Common code for GET and HEAD commands.
This sends the response code and MIME headers.
Return value is either a file object (which has to be copied
to the outputfile by the caller unless the command was HEAD,
and must be closed by the caller under all circumstances), or
None, in which case the caller has nothing further to do."""
path = self.translated_path
if os.path.isdir(path):
self.send_error(403, "Directory listing not supported")
return None
try:
f = open(path, 'rb')
except IOError:
self.send_error(404, "File %s not found" % path)
return None
self.send_response(200)
self.send_header("Content-type", self.guess_type(path))
self.end_headers()
return f
def log_message(self, format, *args):
append_log(self.address_string(), format % args)
monthname = [None,
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
def log_date_time_string():
"""Return the current time formatted for logging."""
now = time.time()
year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
s = "%02d/%3s/%04d %02d:%02d:%02d" % (
day, monthname[month], year, hh, mm, ss)
return s
def append_log(stage, str):
if not log_file: return
msg = "%s - - [%s] %s\n" % (stage, log_date_time_string(), str)
if log_file == "stdout":
print msg,
else:
log = open(log_file, 'a')
log.write(msg)
log.close()
def limit_process():
def dopple(num):
return (num, num)
setrlimit(RLIMIT_DATA, dopple(MAX_HEAP * 1024 * 1024))
setrlimit(RLIMIT_RSS, dopple(MAX_HEAP * 1024 * 1024))
setrlimit(RLIMIT_STACK, dopple(MAX_STACK * 1024 * 1024))
setrlimit(RLIMIT_NPROC, dopple(100))
setrlimit(RLIMIT_NOFILE, dopple(32))
emacs_pid = None
def start_emacs():
global emacs_pid
if debug:
emacs_pid = os.fork()
else:
emacs_pid = pty.fork()[0]
if emacs_pid == 0:
try:
try:
if user and user != os.geteuid():
os.setuid(user)
except:
pass
limit_process()
pargs = ["emacs", "--no-init-file", "--no-site-file"]
if no_wins and not debug:
pargs.append("--no-windows")
pargs.extend(["--unibyte", "--eval", """
(progn
(setq initial-frame-alist '((visibility . nil))
message-log-max nil)
(require 'gnuserv)
(gnuserv-start)
(load "httpd")
(load "cgi" t)
(load "%s" t)
(setq httpd-terminate-emacs nil
httpd-line-terminator "\n"
httpd-document-root "%s"))""" % (startup, html_dir)])
os.execvp(pargs[0], pargs)
finally:
os._exit(1)
os._exit(1) # shouldn't reach here
def kill_emacs(signo = SIGTERM):
global emacs_pid
os.kill(emacs_pid, signo)
os.waitpid(emacs_pid, 0)
emacs_pid = None
def restart_emacs(handler):
try:
kill_emacs(SIGKILL)
handler.log_message("Killed Emacs process pid %d", emacs_pid)
except:
pass
handler.log_message("Starting new Emacs process")
start_emacs()
handler.log_message("New Emacs process is pid %d", emacs_pid)
time.sleep(2)
def gnuserv_socket():
file = "/tmp/gsrvdir%d/gsrv" % user
if os.path.exists(file):
return file
file = "/tmp/gsrv%d" % user
if os.path.exists(file):
return file
return None
# daemonize() is a function which makes the process calling it into a
# "daemon". The process is detached from the current shell and will
# not receive hangup when the shell exits.
#
# Steinar Knutsen, 1997
def daemonize():
import time
pid = os.fork()
if not pid:
otherpid = os.fork()
if not otherpid:
ppid = os.getppid()
while ppid != 1:
time.sleep(0.5)
ppid = os.getppid()
return
else:
os._exit(0)
else:
os.wait()
sys.exit(0)
# provide an HTTP server, so that web users can manipulate the cluster
# as well
from getopt import getopt
(opts, args) = getopt(sys.argv[1:], [],
longopts = [ "help", "port=", "log-file=", "load=",
"daemon", "pidfile=", "user=", "debug" ])
def usage():
print """
Welcome to the server program. Command syntax is:
httpd-serve [options] [server-directory]
The SERVER-DIRECTORY is where your static HTML files (if any) are
kept. This includes stylesheet, images, etc. It will be the "root"
of the httpd server. The default value is /var/www.
Possible options are:
--help Show this page
--debug Enable debugging mode
--daemon Make the HTTP server run as a daemon
--load FILE Eval Emacs Lisp code in FILE during startup
--log-file FILE Output logging information into FILE
--pidfile FILE Write out the process id of httpd-serve to FILE
--port NUMBER Use PORT as the server port; default is 80
--user USER/UID Run Emacs process as USER (default is 'nobody')
"""
sys.exit(0)
for opt in opts:
if opt[0] == "--port":
port = int(opt[1])
elif opt[0] == "--debug":
debug = 1
elif opt[0] == "--daemon":
daemon = 1
elif opt[0] == "--load":
startup = opt[1]
elif opt[0] == "--log-file":
log_file = opt[1]
elif opt[0] == "--pidfile":
pid_file = opt[1]
elif opt[0] == "--user":
user = opt[1]
if not re.match('^[0-9]+$', user):
user = pwd.getpwnam(user)
if user:
user = user[2]
elif opt[0] == "--help":
usage()
if len(args) == 0:
html_dir = "/var/www"
else:
html_dir = args[0]
os.chdir(html_dir)
if not daemon and not log_file:
log_file = "stdout"
# Start the Python HTTP server
if daemon: daemonize()
limit_process()
if pid_file:
fd = open(pid_file, "w")
fd.write("%d\n" % os.getpid())
fd.close()
# Start our dedicated Emacs server
start_emacs()
if no_wins == 1:
time.sleep(5)
if not gnuserv_socket():
no_wins = 0
# Give it a try under X, if available
if not os.environ.has_key('DISPLAY'):
os.environ['DISPLAY'] = ":0"
restart_emacs()
time.sleep(5)
if not gnuserv_socket():
append_log("localhost", "Gnuserv failed to start, aborting")
kill_emacs(SIGKILL)
sys.exit(1)
# Setup signal handlers for the HTTP process
def interrupt(signo, frame):
append_log("localhost", "httpd-serve died with signal %d" % signo)
try: kill_emacs(SIGKILL) # Sorry Emacs, time to go
except: pass
sys.exit(1)
signal(SIGTERM, interrupt)
signal(SIGKILL, interrupt)
signal(SIGQUIT, interrupt)
def ignore(signo, frame): pass
signal(SIGINT, ignore)
signal(SIGHUP, ignore)
class AlarmTimeout(Exception): pass
def timeout(signum, frame):
raise AlarmTimeout()
signal(SIGALRM, timeout)
# Start serving HTTP requests on `port'!
BaseHTTPServer.HTTPServer(('', port), HTTPRequestHandler).serve_forever()