[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/python-emscripten/python/trunk/emscripten_fetch.pyx [Back]  [Original]

# Python wrapper for emscripten_* C functions - Fetch API

# Copyright (C) 2019  Sylvain Beucler

# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.

# http://docs.cython.org/en/latest/src/tutorial/strings.html#auto-encoding-and-decoding
# Most of our strings are converted from/to JS through emscripten stringToUTF8/UTF8ToString
# cython: c_string_type=unicode, c_string_encoding=utf8
# Note: Py->C auto-UTF-8 (instead of .encode('UTF-8')) not supported for Py2
# Note: causes issues with Typed Memoryviews

# TODO: move to emscripten.fetch but requires emscripten/__init__.py,
# patching emscripten_fetch.c, etc.

from __future__ import print_function

from cpython.mem cimport PyMem_Malloc, PyMem_Free
from cpython.ref cimport PyObject, Py_XINCREF, Py_XDECREF

from cpython.buffer cimport PyBuffer_FillInfo

from libc.string cimport strncpy

from libc.stdint cimport uint32_t, uint64_t

cdef extern from "emscripten/html5.h":
    ctypedef int EM_BOOL
    ctypedef int EMSCRIPTEN_RESULT
    enum: EM_TRUE
    enum: EM_FALSE

cdef extern from "emscripten/fetch.h":
    ctypedef struct emscripten_fetch_attr_t:
        char requestMethod[32]
        void *userData
        void (*onsuccess)(emscripten_fetch_t *fetch)
        void (*onerror)(emscripten_fetch_t *fetch)
        void (*onprogress)(emscripten_fetch_t *fetch)
        void (*onreadystatechange)(emscripten_fetch_t *fetch)
        uint32_t attributes
        unsigned long timeoutMSecs
        EM_BOOL withCredentials
        const char *destinationPath
        const char *userName
        const char *password
        const char * const *requestHeaders
        const char *overriddenMimeType
        const char *requestData
        size_t requestDataSize

    ctypedef struct emscripten_fetch_t:
        unsigned int id
        void *userData
        const char *url
        const char *data
        uint64_t numBytes
        uint64_t dataOffset
        uint64_t totalBytes
        unsigned short readyState
        unsigned short status
        char statusText[64]
        uint32_t __proxyState
        emscripten_fetch_attr_t __attributes

    enum:
        EMSCRIPTEN_FETCH_LOAD_TO_MEMORY
        EMSCRIPTEN_FETCH_STREAM_DATA
        EMSCRIPTEN_FETCH_PERSIST_FILE
        EMSCRIPTEN_FETCH_APPEND
        EMSCRIPTEN_FETCH_REPLACE
        EMSCRIPTEN_FETCH_NO_DOWNLOAD
        EMSCRIPTEN_FETCH_SYNCHRONOUS
        EMSCRIPTEN_FETCH_WAITABLE

    void emscripten_fetch_attr_init(emscripten_fetch_attr_t *fetch_attr)
    emscripten_fetch_t *emscripten_fetch(emscripten_fetch_attr_t *fetch_attr, const char *url)
    #EMSCRIPTEN_RESULT emscripten_fetch_wait(emscripten_fetch_t *fetch, double timeoutMSecs)
    EMSCRIPTEN_RESULT emscripten_fetch_close(emscripten_fetch_t *fetch)
    size_t emscripten_fetch_get_response_headers_length(emscripten_fetch_t *fetch)
    size_t emscripten_fetch_get_response_headers(emscripten_fetch_t *fetch, char *dst, size_t dstSizeBytes)
    char **emscripten_fetch_unpack_response_headers(const char *headersString)
    void emscripten_fetch_free_unpacked_response_headers(char **unpackedHeaders)

LOAD_TO_MEMORY = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY
STREAM_DATA = EMSCRIPTEN_FETCH_STREAM_DATA
PERSIST_FILE = EMSCRIPTEN_FETCH_PERSIST_FILE
APPEND = EMSCRIPTEN_FETCH_APPEND
REPLACE = EMSCRIPTEN_FETCH_REPLACE
NO_DOWNLOAD = EMSCRIPTEN_FETCH_NO_DOWNLOAD
SYNCHRONOUS = EMSCRIPTEN_FETCH_SYNCHRONOUS
WAITABLE = EMSCRIPTEN_FETCH_WAITABLE

# Fetch API
# https://emscripten.org/docs/api_reference/fetch.html

# http://docs.cython.org/en/latest/src/userguide/extension_types.html
cdef class Fetch:
    cdef emscripten_fetch_t *fetch
    cdef callbacks

    def __cinit__(self, url, requestMethod=None, userData=None,
            onsuccess=None, onerror=None, onprogress=None, onreadystatechange=None,
            attributes=None, timeoutMSecs=None, withCredentials=None,
            destinationPath=None, userName=None, password=None,
            requestHeaders=None, overriddenMimeType=None, requestData=None):
    
        # Keep track of temporary Python strings we pass emscripten_fetch() for copy
        py_str_refs = []
    
        cdef emscripten_fetch_attr_t attr
        emscripten_fetch_attr_init(&attr)
    
        Py_XINCREF(self)  # survive until callback
        attr.userData = self
    
        if requestMethod is not None:
            strncpy(attr.requestMethod,
                requestMethod.encode('UTF-8'),
                sizeof(attr.requestMethod) - 1)
    
        self.userData = userData
    
        self.callbacks = {}
        attr.onsuccess = callpyfunc_fetch_onsuccess
        attr.onerror = callpyfunc_fetch_onerror
        if onsuccess is not None:
            self.callbacks['onsuccess'] = onsuccess
        if onerror is not None:
            self.callbacks['onerror'] = onerror
        if onprogress is not None:
            self.callbacks['onprogress'] = onprogress
            attr.onprogress = callpyfunc_fetch_onprogress
        if onreadystatechange is not None:
            self.callbacks['onreadystatechange'] = onreadystatechange
            attr.onreadystatechange = callpyfunc_fetch_onreadystatechange
    
        if attributes is not None:
            attr.attributes = attributes
        if timeoutMSecs is not None:
            attr.timeoutMSecs = timeoutMSecs
        if withCredentials is not None:
            attr.withCredentials = withCredentials
        if destinationPath is not None:
            py_str_refs.append(destinationPath.encode('UTF-8'))
            attr.destinationPath = py_str_refs[-1]
        if userName is not None:
            py_str_refs.append(userName.encode('UTF-8'))
            attr.userName = py_str_refs[-1]
        if password is not None:
            py_str_refs.append(password.encode('UTF-8'))
            attr.password = py_str_refs[-1]
    
        cdef char** headers
        if requestHeaders is not None:
            size = (2 * len(requestHeaders) + 1) * sizeof(char*)
            headers = PyMem_Malloc(size)
            i = 0
            for name,value in requestHeaders.items():
                py_str_refs.append(name.encode('UTF-8'))
                headers[i] = py_str_refs[-1]
                i += 1
                py_str_refs.append(value.encode('UTF-8'))
                headers[i] = py_str_refs[-1]
                i += 1
            headers[i] = NULL
            attr.requestHeaders = headers
    
        if overriddenMimeType is not None:
            py_str_refs.append(overriddenMimeType.encode('UTF-8'))
            attr.overriddenMimeType = py_str_refs[-1]
    
        if requestData is not None:
            size = len(requestData)
            attr.requestDataSize = size
            # direct pointer, no UTF-8 encoding pass:
            attr.requestData = requestData
    
        # Fetch
        cdef emscripten_fetch_t *fetch = emscripten_fetch(&attr, url.encode('UTF-8'))
        self.fetch = fetch
    
        # Explicitely deref temporary Python strings.  Test for forgotten refs with e.g.:
        # print(attr.overriddenMimeType, attr.destinationPath, attr.userName, attr.password)
        del py_str_refs
    
        if requestHeaders is not None:
            PyMem_Free(attr.requestHeaders)

    def __dealloc__(self):
        emscripten_fetch_close(self.fetch)

    # Currently unsafe:
    # https://github.com/emscripten-core/emscripten/issues/8234
    #def fetch_close(fetch):
    #    pass

    # http://docs.cython.org/en/latest/src/userguide/buffer.html
    # https://docs.python.org/3/c-api/typeobj.html#c.PyBufferProcs.bf_getbuffer
    # https://docs.python.org/3/c-api/buffer.html#c.PyObject_GetBuffer
    # https://docs.python.org/3/c-api/buffer.html#c.PyBuffer_FillInfo
    def __getbuffer__(self, Py_buffer *view, int flags):
        if self.fetch.data != NULL:
            is_readonly = 1
            PyBuffer_FillInfo(view, self, self.fetch.data, self.fetch.numBytes, is_readonly, flags)
        else:
            view.obj = None
            raise BufferError
    def __releasebuffer__(self, Py_buffer *view):
        pass

    def __repr__(self):
        return u''.format(repr(self.id), repr(self.userData), repr(self.url), self.data and "" or "None", repr(self.numBytes), repr(self.dataOffset), repr(self.totalBytes), repr(self.readyState), repr(self.status), repr(self.statusText))

    # For testing whether a copy occurred:
    #def overwrite(self):
    #    cdef char* overwrite = (self.fetch.data)
    #    overwrite[0] = b'O'

    def get_response_headers(self):
        cdef char* buf = NULL
        # Note: JS crash if applied on a persisted request from IDB cache
        # https://github.com/emscripten-core/emscripten/issues/7026#issuecomment-545488132
        cdef length = emscripten_fetch_get_response_headers_length(self.fetch)
        if length > 0:
            headersString = PyMem_Malloc(length)
            emscripten_fetch_get_response_headers(self.fetch, headersString, length+1)
            ret = headersString[:length]  # copy
            PyMem_Free(headersString)
            return ret
        else:
            return None

    def get_unpacked_response_headers(self):
        cdef char* headersString = NULL
        cdef char** unpackedHeaders = NULL
        # Note: JS crash if applied on a persisted request from IDB cache
        cdef length = emscripten_fetch_get_response_headers_length(self.fetch)
        if length > 0:
            headersString = PyMem_Malloc(length)
            emscripten_fetch_get_response_headers(self.fetch, headersString, length+1)
            unpackedHeaders = emscripten_fetch_unpack_response_headers(headersString)
            PyMem_Free(headersString)
            d = {}
            i = 0
            while unpackedHeaders[i] != NULL:
                k = unpackedHeaders[i]  # c_string_encoding
                i += 1
                v = unpackedHeaders[i]  # c_string_encoding
                i += 1
                d[k] = v
            emscripten_fetch_free_unpacked_response_headers(unpackedHeaders)
            return d
        else:
            return None

    @property
    def id(self):
        return self.fetch.id
    cdef readonly userData
    @property
    def url(self):
        #return self.fetch.url.decode('UTF-8')
        return self.fetch.url  # c_string_encoding
    @property
    def data(self):
        if self.fetch.data != NULL:
            return self
        else:
            return None
    @property
    def numBytes(self):
        return self.fetch.numBytes
    @property
    def dataOffset(self):
        return self.fetch.dataOffset
    @property
    def totalBytes(self):
        return self.fetch.totalBytes  # Content-Length
    @property
    def readyState(self):
        return self.fetch.readyState
    @property
    def status(self):
        return self.fetch.status
    @property
    def statusText(self):
        #return self.fetch.statusText.decode('UTF-8')
        return self.fetch.statusText  # c_string_encoding

cdef void callpyfunc_fetch_callback(emscripten_fetch_t *fetch, char* callback_name):
    cdef Fetch py_fetch = fetch.userData
    # for theoretical concurrency, if we're called during emscripten_fetch()
    py_fetch.fetch = fetch
    # call Python function
    if py_fetch.callbacks.get(callback_name, None):
        py_fetch.callbacks[callback_name](py_fetch)

# one of {onsuccess,onerror} is guaranteed to run, deref Fetch there
cdef void callpyfunc_fetch_onsuccess(emscripten_fetch_t *fetch):
    callpyfunc_fetch_callback(fetch, 'onsuccess')
    Py_XDECREF(fetch.userData)
cdef void callpyfunc_fetch_onerror(emscripten_fetch_t *fetch):
    callpyfunc_fetch_callback(fetch, 'onerror')
    Py_XDECREF(fetch.userData)
cdef void callpyfunc_fetch_onprogress(emscripten_fetch_t *fetch):
    callpyfunc_fetch_callback(fetch, 'onprogress')
cdef void callpyfunc_fetch_onreadystatechange(emscripten_fetch_t *fetch):
    callpyfunc_fetch_callback(fetch, 'onreadystatechange')


# import emscripten_fetch,sys; f=lambda x:sys.stdout.write(repr(x)+"\n");
# #Module.cwrap('PyRun_SimpleString', 'number', ['string'])("def g(x):\n    global a; a=x")
# emscripten_fetch.Fetch('/', onsuccess=f)
# emscripten_fetch.Fetch(u'/hello', onsuccess=f)
# emscripten_fetch.Fetch('/hello', attributes=emscripten_fetch.LOAD_TO_MEMORY, onsuccess=f); del f  # output
# fetch_attr={'onsuccess':f}; emscripten_fetch.Fetch('/hello', **fetch_attr); del fetch_attr['onsuccess']  # output
# emscripten_fetch.Fetch('/non-existent', onerror=lambda x:sys.stdout.write(repr(x)+"\n"))
# emscripten_fetch.Fetch('https://bank.confidential/', onerror=lambda x:sys.stdout.write(repr(x)+"\n"))  # simulated 404
# emscripten_fetch.Fetch('/hello', attributes=emscripten_fetch.LOAD_TO_MEMORY|emscripten_fetch.PERSIST_FILE, onsuccess=f)
# Note: fe.fetch.id changes (in-place) when first caching
# emscripten_fetch.Fetch('/hello', requestMethod='EM_IDB_DELETE', onsuccess=f)
# emscripten_fetch.Fetch('/hello', attributes=emscripten_fetch.LOAD_TO_MEMORY, requestMethod='POST', requestData='AA\xffBB\x00CC', onsuccess=f, onerror=f)
# emscripten_fetch.Fetch('/hello', attributes=emscripten_fetch.LOAD_TO_MEMORY, requestMethod='12345678901234567890123456789012', onerror=f)
# emscripten_fetch.Fetch('/hello', attributes=emscripten_fetch.LOAD_TO_MEMORY, onsuccess=f, userData='userData', overriddenMimeType='text/html', userName='userName', password='password', requestHeaders={'Content-Type':'text/plain','Cache-Control':'no-store'})
# emscripten_fetch.Fetch('/hello', attributes=emscripten_fetch.LOAD_TO_MEMORY|emscripten_fetch.PERSIST_FILE, onsuccess=f, destinationPath='destinationPath'); emscripten_fetch.Fetch('destinationPath', requestMethod='EM_IDB_DELETE', onsuccess=f)
# fe=emscripten_fetch.Fetch('/hello', attributes=emscripten_fetch.LOAD_TO_MEMORY|emscripten_fetch.PERSIST_FILE, onsuccess=f, destinationPath='destinationPath'); fe2=emscripten_fetch.Fetch('destinationPath', requestMethod='EM_IDB_DELETE', onsuccess=f); print("fe=",fe); print("fe2=",fe2)
# Note: fe2 can occur before fe1
# r=emscripten_fetch.Fetch('/hello', attributes=emscripten_fetch.LOAD_TO_MEMORY)
# open('test.txt','wb').write(r); open('test.txt','rb').read()
# r.data != None
# memoryview(r)[:5].tobytes()
# import cStringIO; cStringIO.StringIO(r).read(5)

Web Proxy Viewer  |  New URL  |  Original Page