1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4"""
5Support for creating a service which runs a web server.
6"""
7
8import os
9
10# Twisted Imports
11from twisted.web import server, static, twcgi, script, demo, distrib, wsgi
12from twisted.internet import interfaces, reactor
13from twisted.python import usage, reflect, threadpool
14from twisted.spread import pb
15from twisted.application import internet, service, strports
16
17
18class Options(usage.Options):
19    """
20    Define the options accepted by the I{twistd web} plugin.
21    """
22    synopsis = "[web options]"
23
24    optParameters = [["port", "p", None, "strports description of the port to "
25                      "start the server on."],
26                     ["logfile", "l", None, "Path to web CLF (Combined Log Format) log file."],
27                     ["https", None, None, "Port to listen on for Secure HTTP."],
28                     ["certificate", "c", "server.pem", "SSL certificate to use for HTTPS. "],
29                     ["privkey", "k", "server.pem", "SSL certificate to use for HTTPS."],
30                     ]
31
32    optFlags = [["personal", "",
33                 "Instead of generating a webserver, generate a "
34                 "ResourcePublisher which listens on  the port given by "
35                 "--port, or ~/%s " % (distrib.UserDirectory.userSocketName,) +
36                 "if --port is not specified."],
37                ["notracebacks", "n", "Do not display tracebacks in broken web pages. " +
38                 "Displaying tracebacks to users may be security risk!"],
39                ]
40
41    compData = usage.Completions(
42                   optActions={"logfile" : usage.CompleteFiles("*.log"),
43                               "certificate" : usage.CompleteFiles("*.pem"),
44                               "privkey" : usage.CompleteFiles("*.pem")}
45                   )
46
47    longdesc = """\
48This starts a webserver.  If you specify no arguments, it will be a
49demo webserver that has the Test class from twisted.web.demo in it."""
50
51    def __init__(self):
52        usage.Options.__init__(self)
53        self['indexes'] = []
54        self['root'] = None
55
56
57    def opt_index(self, indexName):
58        """
59        Add the name of a file used to check for directory indexes.
60        [default: index, index.html]
61        """
62        self['indexes'].append(indexName)
63
64    opt_i = opt_index
65
66
67    def opt_user(self):
68        """
69        Makes a server with ~/public_html and ~/.twistd-web-pb support for
70        users.
71        """
72        self['root'] = distrib.UserDirectory()
73
74    opt_u = opt_user
75
76
77    def opt_path(self, path):
78        """
79        <path> is either a specific file or a directory to be set as the root
80        of the web server. Use this if you have a directory full of HTML, cgi,
81        epy, or rpy files or any other files that you want to be served up raw.
82        """
83        self['root'] = static.File(os.path.abspath(path))
84        self['root'].processors = {
85            '.cgi': twcgi.CGIScript,
86            '.epy': script.PythonScript,
87            '.rpy': script.ResourceScript,
88            }
89
90
91    def opt_processor(self, proc):
92        """
93        `ext=class' where `class' is added as a Processor for files ending
94        with `ext'.
95        """
96        if not isinstance(self['root'], static.File):
97            raise usage.UsageError("You can only use --processor after --path.")
98        ext, klass = proc.split('=', 1)
99        self['root'].processors[ext] = reflect.namedClass(klass)
100
101
102    def opt_class(self, className):
103        """
104        Create a Resource subclass with a zero-argument constructor.
105        """
106        classObj = reflect.namedClass(className)
107        self['root'] = classObj()
108
109
110    def opt_resource_script(self, name):
111        """
112        An .rpy file to be used as the root resource of the webserver.
113        """
114        self['root'] = script.ResourceScriptWrapper(name)
115
116
117    def opt_wsgi(self, name):
118        """
119        The FQPN of a WSGI application object to serve as the root resource of
120        the webserver.
121        """
122        try:
123            application = reflect.namedAny(name)
124        except (AttributeError, ValueError):
125            raise usage.UsageError("No such WSGI application: %r" % (name,))
126        pool = threadpool.ThreadPool()
127        reactor.callWhenRunning(pool.start)
128        reactor.addSystemEventTrigger('after', 'shutdown', pool.stop)
129        self['root'] = wsgi.WSGIResource(reactor, pool, application)
130
131
132    def opt_mime_type(self, defaultType):
133        """
134        Specify the default mime-type for static files.
135        """
136        if not isinstance(self['root'], static.File):
137            raise usage.UsageError("You can only use --mime_type after --path.")
138        self['root'].defaultType = defaultType
139    opt_m = opt_mime_type
140
141
142    def opt_allow_ignore_ext(self):
143        """
144        Specify whether or not a request for 'foo' should return 'foo.ext'
145        """
146        if not isinstance(self['root'], static.File):
147            raise usage.UsageError("You can only use --allow_ignore_ext "
148                                   "after --path.")
149        self['root'].ignoreExt('*')
150
151
152    def opt_ignore_ext(self, ext):
153        """
154        Specify an extension to ignore.  These will be processed in order.
155        """
156        if not isinstance(self['root'], static.File):
157            raise usage.UsageError("You can only use --ignore_ext "
158                                   "after --path.")
159        self['root'].ignoreExt(ext)
160
161
162    def postOptions(self):
163        """
164        Set up conditional defaults and check for dependencies.
165
166        If SSL is not available but an HTTPS server was configured, raise a
167        L{UsageError} indicating that this is not possible.
168
169        If no server port was supplied, select a default appropriate for the
170        other options supplied.
171        """
172        if self['https']:
173            try:
174                from twisted.internet.ssl import DefaultOpenSSLContextFactory
175            except ImportError:
176                raise usage.UsageError("SSL support not installed")
177        if self['port'] is None:
178            if self['personal']:
179                path = os.path.expanduser(
180                    os.path.join('~', distrib.UserDirectory.userSocketName))
181                self['port'] = 'unix:' + path
182            else:
183                self['port'] = 'tcp:8080'
184
185
186
187def makePersonalServerFactory(site):
188    """
189    Create and return a factory which will respond to I{distrib} requests
190    against the given site.
191
192    @type site: L{twisted.web.server.Site}
193    @rtype: L{twisted.internet.protocol.Factory}
194    """
195    return pb.PBServerFactory(distrib.ResourcePublisher(site))
196
197
198
199def makeService(config):
200    s = service.MultiService()
201    if config['root']:
202        root = config['root']
203        if config['indexes']:
204            config['root'].indexNames = config['indexes']
205    else:
206        # This really ought to be web.Admin or something
207        root = demo.Test()
208
209    if isinstance(root, static.File):
210        root.registry.setComponent(interfaces.IServiceCollection, s)
211
212    if config['logfile']:
213        site = server.Site(root, logPath=config['logfile'])
214    else:
215        site = server.Site(root)
216
217    site.displayTracebacks = not config["notracebacks"]
218
219    if config['personal']:
220        personal = strports.service(
221            config['port'], makePersonalServerFactory(site))
222        personal.setServiceParent(s)
223    else:
224        if config['https']:
225            from twisted.internet.ssl import DefaultOpenSSLContextFactory
226            i = internet.SSLServer(int(config['https']), site,
227                          DefaultOpenSSLContextFactory(config['privkey'],
228                                                       config['certificate']))
229            i.setServiceParent(s)
230        strports.service(config['port'], site).setServiceParent(s)
231
232    return s
233