1# -*- test-case-name: twisted.web.test.test_tap -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Support for creating a service which runs a web server.
7"""
8
9
10import os
11import warnings
12
13import incremental
14
15from twisted.application import service, strports
16from twisted.internet import interfaces, reactor
17from twisted.python import deprecate, reflect, threadpool, usage
18from twisted.spread import pb
19from twisted.web import demo, distrib, resource, script, server, static, twcgi, wsgi
20
21
22class Options(usage.Options):
23    """
24    Define the options accepted by the I{twistd web} plugin.
25    """
26
27    synopsis = "[web options]"
28
29    optParameters = [
30        ["logfile", "l", None, "Path to web CLF (Combined Log Format) log file."],
31        [
32            "certificate",
33            "c",
34            "server.pem",
35            "(DEPRECATED: use --listen) " "SSL certificate to use for HTTPS. ",
36        ],
37        [
38            "privkey",
39            "k",
40            "server.pem",
41            "(DEPRECATED: use --listen) " "SSL certificate to use for HTTPS.",
42        ],
43    ]
44
45    optFlags = [
46        [
47            "notracebacks",
48            "n",
49            (
50                "(DEPRECATED: Tracebacks are disabled by default. "
51                "See --enable-tracebacks to turn them on."
52            ),
53        ],
54        [
55            "display-tracebacks",
56            "",
57            (
58                "Show uncaught exceptions during rendering tracebacks to "
59                "the client. WARNING: This may be a security risk and "
60                "expose private data!"
61            ),
62        ],
63    ]
64
65    optFlags.append(
66        [
67            "personal",
68            "",
69            "Instead of generating a webserver, generate a "
70            "ResourcePublisher which listens on  the port given by "
71            "--listen, or ~/%s " % (distrib.UserDirectory.userSocketName,)
72            + "if --listen is not specified.",
73        ]
74    )
75
76    compData = usage.Completions(
77        optActions={
78            "logfile": usage.CompleteFiles("*.log"),
79            "certificate": usage.CompleteFiles("*.pem"),
80            "privkey": usage.CompleteFiles("*.pem"),
81        }
82    )
83
84    longdesc = """\
85This starts a webserver.  If you specify no arguments, it will be a
86demo webserver that has the Test class from twisted.web.demo in it."""
87
88    def __init__(self):
89        usage.Options.__init__(self)
90        self["indexes"] = []
91        self["root"] = None
92        self["extraHeaders"] = []
93        self["ports"] = []
94        self["port"] = self["https"] = None
95
96    def opt_port(self, port):
97        """
98        (DEPRECATED: use --listen)
99        Strports description of port to start the server on
100        """
101        msg = deprecate.getDeprecationWarningString(
102            self.opt_port, incremental.Version("Twisted", 18, 4, 0)
103        )
104        warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
105        self["port"] = port
106
107    opt_p = opt_port
108
109    def opt_https(self, port):
110        """
111        (DEPRECATED: use --listen)
112        Port to listen on for Secure HTTP.
113        """
114        msg = deprecate.getDeprecationWarningString(
115            self.opt_https, incremental.Version("Twisted", 18, 4, 0)
116        )
117        warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
118        self["https"] = port
119
120    def opt_listen(self, port):
121        """
122        Add an strports description of port to start the server on.
123        [default: tcp:8080]
124        """
125        self["ports"].append(port)
126
127    def opt_index(self, indexName):
128        """
129        Add the name of a file used to check for directory indexes.
130        [default: index, index.html]
131        """
132        self["indexes"].append(indexName)
133
134    opt_i = opt_index
135
136    def opt_user(self):
137        """
138        Makes a server with ~/public_html and ~/.twistd-web-pb support for
139        users.
140        """
141        self["root"] = distrib.UserDirectory()
142
143    opt_u = opt_user
144
145    def opt_path(self, path):
146        """
147        <path> is either a specific file or a directory to be set as the root
148        of the web server. Use this if you have a directory full of HTML, cgi,
149        epy, or rpy files or any other files that you want to be served up raw.
150        """
151        self["root"] = static.File(os.path.abspath(path))
152        self["root"].processors = {
153            ".epy": script.PythonScript,
154            ".rpy": script.ResourceScript,
155        }
156        self["root"].processors[".cgi"] = twcgi.CGIScript
157
158    def opt_processor(self, proc):
159        """
160        `ext=class' where `class' is added as a Processor for files ending
161        with `ext'.
162        """
163        if not isinstance(self["root"], static.File):
164            raise usage.UsageError("You can only use --processor after --path.")
165        ext, klass = proc.split("=", 1)
166        self["root"].processors[ext] = reflect.namedClass(klass)
167
168    def opt_class(self, className):
169        """
170        Create a Resource subclass with a zero-argument constructor.
171        """
172        classObj = reflect.namedClass(className)
173        self["root"] = classObj()
174
175    def opt_resource_script(self, name):
176        """
177        An .rpy file to be used as the root resource of the webserver.
178        """
179        self["root"] = script.ResourceScriptWrapper(name)
180
181    def opt_wsgi(self, name):
182        """
183        The FQPN of a WSGI application object to serve as the root resource of
184        the webserver.
185        """
186        try:
187            application = reflect.namedAny(name)
188        except (AttributeError, ValueError):
189            raise usage.UsageError(f"No such WSGI application: {name!r}")
190        pool = threadpool.ThreadPool()
191        reactor.callWhenRunning(pool.start)
192        reactor.addSystemEventTrigger("after", "shutdown", pool.stop)
193        self["root"] = wsgi.WSGIResource(reactor, pool, application)
194
195    def opt_mime_type(self, defaultType):
196        """
197        Specify the default mime-type for static files.
198        """
199        if not isinstance(self["root"], static.File):
200            raise usage.UsageError("You can only use --mime_type after --path.")
201        self["root"].defaultType = defaultType
202
203    opt_m = opt_mime_type
204
205    def opt_allow_ignore_ext(self):
206        """
207        Specify whether or not a request for 'foo' should return 'foo.ext'
208        """
209        if not isinstance(self["root"], static.File):
210            raise usage.UsageError(
211                "You can only use --allow_ignore_ext " "after --path."
212            )
213        self["root"].ignoreExt("*")
214
215    def opt_ignore_ext(self, ext):
216        """
217        Specify an extension to ignore.  These will be processed in order.
218        """
219        if not isinstance(self["root"], static.File):
220            raise usage.UsageError("You can only use --ignore_ext " "after --path.")
221        self["root"].ignoreExt(ext)
222
223    def opt_add_header(self, header):
224        """
225        Specify an additional header to be included in all responses. Specified
226        as "HeaderName: HeaderValue".
227        """
228        name, value = header.split(":", 1)
229        self["extraHeaders"].append((name.strip(), value.strip()))
230
231    def postOptions(self):
232        """
233        Set up conditional defaults and check for dependencies.
234
235        If SSL is not available but an HTTPS server was configured, raise a
236        L{UsageError} indicating that this is not possible.
237
238        If no server port was supplied, select a default appropriate for the
239        other options supplied.
240        """
241        if self["port"] is not None:
242            self["ports"].append(self["port"])
243        if self["https"] is not None:
244            try:
245                reflect.namedModule("OpenSSL.SSL")
246            except ImportError:
247                raise usage.UsageError("SSL support not installed")
248            sslStrport = "ssl:port={}:privateKey={}:certKey={}".format(
249                self["https"],
250                self["privkey"],
251                self["certificate"],
252            )
253            self["ports"].append(sslStrport)
254        if len(self["ports"]) == 0:
255            if self["personal"]:
256                path = os.path.expanduser(
257                    os.path.join("~", distrib.UserDirectory.userSocketName)
258                )
259                self["ports"].append("unix:" + path)
260            else:
261                self["ports"].append("tcp:8080")
262
263
264def makePersonalServerFactory(site):
265    """
266    Create and return a factory which will respond to I{distrib} requests
267    against the given site.
268
269    @type site: L{twisted.web.server.Site}
270    @rtype: L{twisted.internet.protocol.Factory}
271    """
272    return pb.PBServerFactory(distrib.ResourcePublisher(site))
273
274
275class _AddHeadersResource(resource.Resource):
276    def __init__(self, originalResource, headers):
277        self._originalResource = originalResource
278        self._headers = headers
279
280    def getChildWithDefault(self, name, request):
281        for k, v in self._headers:
282            request.responseHeaders.addRawHeader(k, v)
283        return self._originalResource.getChildWithDefault(name, request)
284
285
286def makeService(config):
287    s = service.MultiService()
288    if config["root"]:
289        root = config["root"]
290        if config["indexes"]:
291            config["root"].indexNames = config["indexes"]
292    else:
293        # This really ought to be web.Admin or something
294        root = demo.Test()
295
296    if isinstance(root, static.File):
297        root.registry.setComponent(interfaces.IServiceCollection, s)
298
299    if config["extraHeaders"]:
300        root = _AddHeadersResource(root, config["extraHeaders"])
301
302    if config["logfile"]:
303        site = server.Site(root, logPath=config["logfile"])
304    else:
305        site = server.Site(root)
306
307    if config["display-tracebacks"]:
308        site.displayTracebacks = True
309
310    # Deprecate --notracebacks/-n
311    if config["notracebacks"]:
312        msg = deprecate._getDeprecationWarningString(
313            "--notracebacks", incremental.Version("Twisted", 19, 7, 0)
314        )
315        warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
316
317    if config["personal"]:
318        site = makePersonalServerFactory(site)
319    for port in config["ports"]:
320        svc = strports.service(port, site)
321        svc.setServiceParent(s)
322    return s
323