B
    ^m                 @  s  d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddlZddlZddlZddlZddlZddlZejdk red dZdZdZd	Zd
Zde dZG dd dZG dd dZejG dd dZejG dd dZ G dd dZ!G dd de!Z"G dd dZ#G dd dZ$dddd d!Z%d0dd#d#d#d#d$d%d&d'Z&d(d)d*d+Z'd,d)d-d.Z(e)d/kre(  dS )1a  
Jetforce, an experimental Gemini server.

Overview
--------

GeminiServer:
    An asynchronous TCP server built on top of python's asyncio stream
    abstraction. This is a lightweight class that accepts incoming requests,
    logs them, and sends them to a configurable request handler to be processed.

GeminiRequestHandler:
    The request handler manages the life of a single gemini request. It exposes
    a simplified interface to read the request URL and write the gemini response
    status line and body to the socket. The request URL and other server
    information is stuffed into an ``environ`` dictionary that encapsulates the
    request at a low level. This dictionary, along with a callback to write the
    response data, and passed to a configurable "application" function or class.

JetforceApplication:
    This is a base class for writing jetforce server applications. It doesn't
    anything on its own, but it does provide a convenient interface to define
    custom server endpoints using route decorators. If you want to utilize
    jetforce as a library and write your own server in python, this is the class
    that you want to extend. The examples/ directory contains some examples of
    how to accomplish this.

StaticDirectoryApplication:
    This is a pre-built application that serves files from a static directory.
    It provides an "out-of-the-box" gemini server without needing to write any
    lines of code. This is what is invoked when you launch jetforce from the
    command line.
    )annotationsN)      z*Fatal Error: jetforce requires Python 3.7+z0.2.2zJetforce Gemini ServerzMichael LazarzFloodgap Free Software Licensez(c) 2020 Michael Lazara   
You are now riding on...
_________    _____________
______  /______  /___  __/_______________________
___ _  /_  _ \  __/_  /_ _  __ \_  ___/  ___/  _ \
/ /_/ / /  __/ /_ _  __/ / /_/ /  /   / /__ /  __/
\____/  \___/\__/ /_/    \____//_/    \___/ \___/

An Experimental Gemini Server, vz+
https://github.com/michael-lazar/jetforce
c               @  sd   e Zd ZdZdZdZdZdZdZdZ	dZ
d	Zd
ZdZdZdZdZdZdZdZdZdZdZdZdZdS )Statusz'
    Gemini response status codes.
    
               (   )   *   +   ,   2   3   4   5   ;   <   =   >   ?   @   A   N)__name__
__module____qualname____doc__INPUTSUCCESSZSUCCESS_END_OF_SESSIONREDIRECT_TEMPORARYREDIRECT_PERMANENTTEMPORARY_FAILUREZSERVER_UNAVAILABLE	CGI_ERRORZPROXY_ERRORZ	SLOW_DOWNPERMANENT_FAILURE	NOT_FOUNDZGONEPROXY_REQUEST_REFUSEDBAD_REQUESTZCLIENT_CERTIFICATE_REQUIREDZTRANSIENT_CERTIFICATE_REQUESTEDAUTHORISED_CERTIFICATE_REQUIREDZCERTIFICATE_NOT_ACCEPTEDZFUTURE_CERTIFICATE_REJECTEDZEXPIRED_CERTIFICATE_REJECTED r*   r*    /var/gopher/jetforce/jetforce.pyr   L   s,   r   c               @  s   e Zd ZdZddddZdS )RequestzM
    Object that encapsulates information about a single gemini request.
    dict)environc             C  s~   || _ |d | _tj| j}|js,td|js:d| _n|j| _|j| _|j| _|j	| _	|j
| _
tj|j| _|j| _d S )N
GEMINI_URLz"URL must contain a `hostname` partgemini)r.   urlurllibparseurlparsehostname
ValueErrorschemeportpathparamsunquotequeryZfragment)selfr.   	url_partsr*   r*   r+   __init__r   s    
zRequest.__init__N)r   r   r   r   r?   r*   r*   r*   r+   r,   m   s   r,   c               @  s.   e Zd ZU dZded< ded< dZded< dS )	ResponsezN
    Object that encapsulates information about a single gemini response.
    intstatusstrmetaNz6typing.Union[None, bytes, str, typing.Iterator[bytes]]body)r   r   r   r   __annotations__rE   r*   r*   r*   r+   r@      s   
r@   c               @  sj   e Zd ZU dZdZded< dZded< dZded	< d
Zded< d
Z	ded< dZ
ded< dddddZdS )RoutePatternzF
    A pattern for matching URLs with a single endpoint or route.
    z.*rC   r9   r0   r7   Nztyping.Optional[str]r5   Tboolstrict_hostnamestrict_portFstrict_trailing_slashr,   ztyping.Optional[re.Match])requestreturnc             C  s   | j dkr|jd }n| j }t|jd }| jr>|j |kr>dS | jr\|jdk	r\|j|kr\dS | jrr| j|jkrrdS | jr|j}n|j	d}t
| j|S )zL
        Check if the given request URL matches this route pattern.
        NHOSTNAMESERVER_PORT/)r5   r.   rA   rI   rJ   r8   r7   rK   r9   rstripre	fullmatch)r=   rL   Zserver_hostnameZserver_portZrequest_pathr*   r*   r+   match   s    

zRoutePattern.match)r   r   r   r   r9   rF   r7   r5   rI   rJ   rK   rT   r*   r*   r*   r+   rG      s   
rG   c               @  sT   e Zd ZdZdd Zdddddd	ZddddddddddZdddddZdS )JetforceApplicationa  
    Base Jetforce application class with primitive URL routing.

    This is a base class for writing jetforce server applications. It doesn't
    anything on its own, but it does provide a convenient interface to define
    custom server endpoints using route decorators. If you want to utilize
    jetforce as a library and write your own server in python, this is the class
    that you want to extend. The examples/ directory contains some examples of
    how to accomplish this.
    c             C  s
   g | _ d S )N)routes)r=   r*   r*   r+   r?      s    zJetforceApplication.__init__r-   ztyping.Callableztyping.Iterator[bytes])r.   send_statusrM   c             c  s   yt |}W n  tk
r,   |tjd d S X x.| jd d d D ]\}}||r@P q@W | j}||}||j|j t	|j
tr|j
V  n,t	|j
tr|j
 V  n|j
r|j
E d H  d S )NzUnrecognized URL format)r,   	Exceptionr   r(   rV   rT   default_callbackrB   rD   
isinstancerE   bytesrC   encode)r=   r.   rW   rL   route_patterncallbackresponser*   r*   r+   __call__   s"    

zJetforceApplication.__call__.*r0   NTFrC   ztyping.Optional[str]rH   )r9   r7   r5   rI   rK   rM   c               s*   t ||||| ddd fdd}|S )a  
        Decorator for binding a function to a route based on the URL path.

            app = JetforceApplication()

            @app.route('/my-path')
            def my_path(request):
                return Response(Status.SUCCESS, 'text/plain', 'Hello world!')
        ztyping.Callable)funcrM   c               s   j  | f | S )N)rV   append)rc   )r^   r=   r*   r+   wrap   s    z'JetforceApplication.route.<locals>.wrap)rG   )r=   r9   r7   r5   rI   rK   re   r*   )r^   r=   r+   route   s    zJetforceApplication.router,   r@   )rL   rM   c             C  s   t tjdS )z?
        Set the error response based on the URL type.
        z	Not Found)r@   r   r%   )r=   rL   r*   r*   r+   rZ      s    z$JetforceApplication.default_callback)rb   r0   NTF)r   r   r   r   r?   ra   rf   rZ   r*   r*   r*   r+   rU      s   
    rU   c                  s   e Zd ZdZddddd fddZd	d
dddZddd
dddZdddddZddddddZdddddZ	d	d
dddZ
  ZS ) StaticDirectoryApplicationa  
    Application for serving static files & CGI over gemini.

    This is a pre-built application that serves files from a static directory.
    It provides an "out-of-the-box" gemini server without needing to write any
    lines of code. This is what is invoked when you launch jetforce from the
    command line.

    If a directory contains a file with the name "index.gmi", that file will
    be returned when the directory path is requested. Otherwise, a directory
    listing will be auto-generated.
    /var/gemini	index.gmicgi-binrC   )root_directory
index_filecgi_directoryc               sr   t    | jt | jf t|jdd| _	|
dd | _|| _t | _| jdd | jdd d S )NT)strictrP   ztext/geminiz.gmiz.gemini)superr?   rV   rd   rG   serve_static_filepathlibPathresolverootstriprm   rl   	mimetypesZ	MimeTypesZadd_type)r=   rk   rl   rm   )	__class__r*   r+   r?     s    

z#StaticDirectoryApplication.__init__r,   r@   )rL   rM   c             C  s  t |jd}t tjt|}| s@t|j	drLt
tjdS | j| }yt|tjsrt
tjdS W n tk
r   t
tjdS X | rt|	| j}t|tj}|r|r| ||jS | |j}| |}t
tj||S | r|jds:tj|j}	|	j|jd d}	t
tj |	! S || j" }
|
# rf| |
}t
tjd|S | $||}t
tjd|S t
tjdS dS )z
        Convert a URL into a filesystem path, and attempt to serve the file
        or directory that is represented at that path.
        rP   z..z	Not Found)r9   ztext/geminiN)%rq   rr   r9   ru   osnormpathrC   Zis_absolutename
startswithr@   r   r&   rt   accessR_OKOSErroris_filerm   X_OKrun_cgi_scriptr.   guess_mimetype	load_filer    is_direndswithr2   r3   r4   r1   _replacer"   Zgeturlrl   existslist_directory)r=   rL   url_pathfilenamefilesystem_pathZis_cgiZis_exeZmimetype	generatorr>   rl   r*   r*   r+   rp   %  s<    





z,StaticDirectoryApplication.serve_static_filezpathlib.Pathr-   )r   r.   rM   c             C  s   t |}| }d|d< ||d< tj|gtj|dddd}|j  }|jdd}t	|d	ksl|d
 
 sxttjdS |\}}	tj|jddd}
tt||	|
S )zu
        Execute the given file as a CGI script and return the script's stdout
        stream to the client.
        zGCI/1.1ZGATEWAY_INTERFACEZSCRIPT_NAME   Tsurrogateescape)stdoutenvbufsizeZuniversal_newlineserrors)maxsplit   r   zUnexpected Errorzutf-8)encodingr   )rC   copy
subprocessPopenPIPEr   readlineru   splitlen	isdecimalr@   r   r$   codecs
iterencoderA   )r=   r   r.   Zscript_nameZcgi_envoutZstatus_lineZstatus_partsrB   rD   rE   r*   r*   r+   r   V  s$    z)StaticDirectoryApplication.run_cgi_scriptztyping.Iterator[bytes])r   rM   c          	   c  s>   | d*}|d}x|r.|V  |d}qW W dQ R X dS )zZ
        Load a file using a generator to allow streaming data to the TCP socket.
        rbi   N)openread)r=   r   fpdatar*   r*   r+   r   v  s
    
z$StaticDirectoryApplication.load_file)r   r   rM   c             c  s   d| d  V  |j|kr0d|j d  V  xnt| D ]^}|jdrRq>q>| r|d||j  d|j d  V  q>d||j  d|j d  V  q>W d	S )
z`
        Auto-generate a text/gemini document based on the contents of the file system.
        zDirectory: /z
z=>/z	..
.z/	z/
	N)r]   parentsortedZiterdirrz   r{   r   )r=   r   r   filer*   r*   r+   r     s    
"z)StaticDirectoryApplication.list_directory)r   rM   c             C  s.   | j |\}}|r"| d| S |p(dS dS )zK
        Guess the mimetype of a file based on the file extension.
        z
; charset=z
text/plainN)rv   Z
guess_type)r=   r   Zmimer   r*   r*   r+   r     s    z)StaticDirectoryApplication.guess_mimetypec             C  sd   |j dkrttjdS |j|jd kr2ttjdS |jrT|j|jd krTttjdS ttjdS dS )z
        Since the StaticDirectoryApplication only serves gemini URLs, return
        a proxy request refused for suspicious URLs.
        r0   z)This server does not allow proxy requestsrN   rO   z	Not FoundN)r7   r@   r   r'   r5   r.   r8   r&   )r=   rL   r*   r*   r+   rZ     s    
z+StaticDirectoryApplication.default_callback)rh   ri   rj   )r   r   r   r   r?   rp   r   r   r   r   rZ   __classcell__r*   r*   )rw   r+   rg     s     1 

rg   c               @  s   e Zd ZU dZdZded< ded< ded< d	ed
< ded< d	ed< ded< d	ed< d	ed< ded< ddddddZddddddZddddZddd d!Z	dd	dd"d#d$Z
d%dd&d'd(Zddd)d*Zddd+d,Zddd-d.Zd/S )0GeminiRequestHandleraV  
    Handle a single Gemini Protocol TCP request.

    The request handler manages the life of a single gemini request. It exposes
    a simplified interface to read the request URL and write the gemini response
    status line and body to the socket. The request URL and other server
    information is stuffed into an ``environ`` dictionary that encapsulates the
    request at a low level. This dictionary, along with a callback to write the
    response data, and passed to a configurable "application" function or class.

    This design borrows heavily from the standard library's HTTP request
    handler (http.server.BaseHTTPRequestHandler). However, I did not make any
    attempts to directly emulate the existing conventions, because Gemini is an
    inherently simpler protocol than HTTP and much of the boilerplate could be
    removed.
    z%d/%b/%Y:%H:%M:%S %zzasyncio.StreamReaderreaderzasyncio.StreamWriterwriterztime.struct_timereceived_timestamprC   remote_addrr-   client_certr1   rA   rB   rD   response_bufferresponse_sizeGeminiServerztyping.CallableNone)serverapprM   c             C  s   || _ || _d| _d S )Nr   )r   r   r   )r=   r   r   r*   r*   r+   r?     s    zGeminiRequestHandler.__init__)r   r   rM   c               s   || _ || _|dd | _|d| _t | _y|  I dH  W n, t	k
rp   | 
tjd |  I dH S X zby8|  }| || j
}x|D ]}| |I dH  qW W n$ t	k
r   | 
tjd  Y nX W d|  I dH  X dS )a  
        Main method for the request handler, performs the following:

            1. Read the request bytes from the reader stream
            2. Parse the request and generate response data
            3. Write the response bytes to the writer stream
        Zpeernamer   ZpeercertNzMalformed requestzAn unexpected error occurred)r   r   Zget_extra_infor   r   time	localtimer   parse_headerrY   write_statusr   r(   close_connectionbuild_environr   
write_bodyr$   )r=   r   r   r.   r   r   r*   r*   r+   handle  s&    


zGeminiRequestHandler.handleztyping.Dict[str, typing.Any])rM   c             C  s   t j| j}| j| jj|j|j| j| j| jjt	| jj
ddt d
}| jrtdd | jd D }|d|dd	| jd
 | jd | jd d |S )z
        Construct a dictionary that will be passed to the application handler.

        Variable names conform to the CGI spec defined in RFC 3875.
        ZGEMINIz	jetforce/)
r/   rN   Z	PATH_INFOZQUERY_STRINGZREMOTE_ADDRZREMOTE_HOSTZSERVER_NAMErO   ZSERVER_PROTOCOLZSERVER_SOFTWAREc             s  s   | ]}|d  V  qdS )r   Nr*   ).0xr*   r*   r+   	<genexpr>  s    z5GeminiRequestHandler.build_environ.<locals>.<genexpr>subjectZCERTIFICATEZ
commonName Z	notBeforeZnotAfterZserialNumber)Z	AUTH_TYPEREMOTE_USERZTLS_CLIENT_NOT_BEFOREZTLS_CLIENT_NOT_AFTERTLS_CLIENT_SERIAL_NUMBER)r2   r3   r4   r1   r   r5   r9   r<   r   rC   r8   __version__r   r-   updateget)r=   r>   r.   r   r*   r*   r+   r     s(    

z"GeminiRequestHandler.build_environc               s@   | j dI dH }|dd }t|dkr2td| | _dS )zq
        Parse the gemini header line.

        The request is a single UTF-8 line formatted as: <URL>

        s   
Ni   z$URL exceeds max length of 1024 bytes)r   Z	readuntilr   r6   decoder1   )r=   r   r*   r*   r+   r     s
    z!GeminiRequestHandler.parse_header)rB   rD   rM   c             C  s"   || _ || _| d| d| _dS )aJ  
        Write the gemini status line to an internal buffer.

        The status line is a single UTF-8 line formatted as:
            <code>	<meta>


        If the response status is 2, the meta field will contain the mimetype
        of the response data sent. If the status is something else, the meta
        will contain a descriptive message.

        The status is not written immediately, it's added to an internal buffer
        that must be flushed. This is done so that the status can be updated as
        long as no other data has been written to the stream yet.
        r   z
N)rB   rD   r   )r=   rB   rD   r*   r*   r+   r   *  s    z!GeminiRequestHandler.write_statusr\   )r   rM   c               s@   |   I dH  |  jt|7  _| j| | j I dH  dS )z:
        Write bytes to the gemini response body.
        N)flush_statusr   r   r   writedrain)r=   r   r*   r*   r+   r   =  s    zGeminiRequestHandler.write_bodyc               sN   | j rD| jsD| j  }|  jt|7  _| j| | j I dH  d| _ dS )zV
        Flush the status line from the internal buffer to the socket stream.
        Nr   )r   r   r]   r   r   r   r   )r=   r   r*   r*   r+   r   F  s    
z!GeminiRequestHandler.flush_statusc               s*   |   I dH  |   | j I dH  dS )zA
        Flush any remaining bytes and close the stream.
        N)r   log_requestr   r   )r=   r*   r*   r+   r   Q  s    z%GeminiRequestHandler.close_connectionc             C  sb   yH| j | j dt| j| j d| j d| j d| j	 d| j
  W n tk
r\   Y nX dS )zY
        Log a gemini request using a format derived from the Common Log Format.
        z [z] "z" z "N)r   log_messager   r   strftimeTIMESTAMP_FORMATr   r1   rB   rD   r   AttributeError)r=   r*   r*   r+   r   Y  s
    Bz GeminiRequestHandler.log_requestN)r   r   r   r   r   rF   r?   r   r   r   r   r   r   r   r   r*   r*   r*   r+   r     s*   
""	r   c               @  s^   e Zd ZdZeZddddd	dd
dddZd
dddZddd
dddZdd
dddZ	dS )r   z
    An asynchronous TCP server that uses the asyncio stream abstraction.

    This is a lightweight class that accepts incoming requests, logs them, and
    sends them to a configurable request handler to be processed.
    	127.0.0.1  N	localhostztyping.CallablerC   rA   zssl.SSLContextr   )r   hostr8   ssl_contextr5   rM   c             C  s"   || _ || _|| _|| _|| _d S )N)r   r8   r5   r   r   )r=   r   r   r8   r   r5   r*   r*   r+   r?   u  s
    	zGeminiServer.__init__)rM   c          
     s   |  t tj| j| j| j| jdI dH }|  d| j  xV|j	D ]L}|
 ^}}}|jtjkrz|  d| d|  qD|  d| d|  qDW |4 I dH  | I dH  W dQ I dH R X dS )z4
        The main asynchronous server loop.
        )sslNzServer hostname is zListening on :zListening on [z]:)r   ABOUTasyncioZstart_serveraccept_connectionr   r8   r   r5   ZsocketsgetsocknamefamilysocketAF_INETZserve_forever)r=   r   sockZsock_ipZ	sock_port_r*   r*   r+   run  s    
zGeminiServer.runzasyncio.StreamReaderzasyncio.StreamWriter)r   r   rM   c               s4   |  | | j}z|||I dH  W d|  X dS )zU
        Hook called by the socket server when a new connection is accepted.
        N)request_handler_classr   r   close)r=   r   r   Zrequest_handlerr*   r*   r+   r     s    zGeminiServer.accept_connection)messagerM   c             C  s   t |tjd dS )z2
        Log a diagnostic server message.
        )r   N)printsysstderr)r=   r   r*   r*   r+   r     s    zGeminiServer.log_message)r   r   Nr   )
r   r   r   r   r   r   r?   r   r   r   r*   r*   r*   r+   r   k  s      	r   rC   ztyping.Tuple[str, str])r5   rM   c             C  s   t t |  d }t t |  d }| r@| sttd|  tjd| d| d|  dgddd	 t|t|fS )
z
    Utility function to generate a self-signed SSL certificate key pair if
    one isn't provided. Results may vary depending on your version of OpenSSL.
    z.crtz.keyz"Writing ad hoc TLS certificate to z,openssl req -newkey rsa:2048 -nodes -keyout z -nodes -x509 -out z -subj "/CN="T)shellcheck)	rq   rr   tempfileZ
gettempdirr   r   r   r   rC   )r5   certfilekeyfiler*   r*   r+   generate_ad_hoc_certificate  s    r   r   ztyping.Optional[str]zssl.SSLContext)r5   r   r   cafilecapathrM   c             C  sZ   |dkrt | \}}t }tj|_||| |sJ|sJ|jtjjd n|	|| |S )ay  
    Generate a sane default SSL context for a Gemini server.

    For more information on what these variables mean and what values they can
    contain, see the python standard library documentation:

        https://docs.python.org/3/library/ssl.html#ssl-contexts

    verify_mode: ssl.CERT_OPTIONAL
        A client certificate request is sent to the client. The client may
        either ignore the request or send a certificate in order perform TLS
        client cert authentication. If the client chooses to send a certificate,
        it is verified. Any verification error immediately aborts the TLS
        handshake.
    N)purpose)
r   r   Z
SSLContextZCERT_OPTIONALZverify_modeZload_cert_chainZload_default_certsZPurposeZCLIENT_AUTHZload_verify_locations)r5   r   r   r   r   contextr*   r*   r+   make_ssl_context  s    r   zargparse.ArgumentParser)rM   c              C  s   t jddt jd} | jddddt d | jd	d
dd | jddtdd | jdddd | jddddd | jddddd | jddddd | jdd d!d"d | S )#z
    Construct the default argument parser when launching the server from
    the command line. These are meant to be application-agnostic arguments
    that could apply to any subclass of the JetforceApplication.
    jetforcez&An Experimental Gemini Protocol Server)progdescriptionZformatter_classz-Vz	--versionversionz	jetforce )actionr   z--hostzServer address to bind toz	127.0.0.1)helpdefaultz--portzServer port to bind toi  )r   typer   z
--hostnamezServer hostnamer   z--tls-certfiler   zServer TLS certificate fileFILE)destr   metavarz--tls-keyfiler   zServer TLS private key filez--tls-cafiler   z'A CA file to use for validating clientsz--tls-capathr   z6A directory containing CA files for validating clientsDIR)argparseArgumentParserZArgumentDefaultsHelpFormatteradd_argumentr   rA   )parserr*   r*   r+   command_line_parser  s<    
r  r   c              C  s   t  } | jddddd | jddddd | jd	d
ddd |  }t|j|j|j}t|j|j	|j
|j|j}t|j|j||j|d}t|  dS )z>
    Entry point for running the static directory server.
    z--dirz)Root directory on the filesystem to servez/var/geminir  )r   r   r  z	--cgi-dirz=CGI script directory, relative to the server's root directoryzcgi-binz--index-filezpIf a directory contains a file with this name, that file will be served instead of auto-generating an index pagez	index.gmir   )r   r8   r   r5   r   N)r  r  
parse_argsrg   dirrl   Zcgi_dirr   r5   r   r   r   r   r   r   r8   r   r   )r  argsr   r   r   r*   r*   r+   
run_server  s6    r  __main__)r   NNNN)*r   
__future__r   r  r   r   Zdataclassesrv   rx   rq   rR   r   r   r   r   r   r   typingZurllib.parser2   version_infoexitr   Z	__title__
__author__Z__license__Z__copyright__r   r   r,   Z	dataclassr@   rG   rU   rg   r   r   r   r   r  r  r   r*   r*   r*   r+   <module>"   sZ   

!(K 0 7@    ",(
