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