UNPKG

pxt-common-packages

Version:
306 lines (262 loc) 8.99 kB
namespace net { /** * Pings a web site * @param dest host name * @param ttl */ //% blockId=netping block="net ping $dest" export function ping(dest: string, ttl: number = 250): number { net.log(`ping ${dest}`); const c = net.instance().controller; if (!c) return Infinity; // don't crash. try { return c.ping(dest, ttl); } catch (e) { console.error("" + e) return Infinity; } } export class Response { _cached: Buffer status_code: number reason: string _read_so_far: number headers: StringMap; /** * The response from a request, contains all the headers/content */ constructor(private socket: Socket) { this._cached = null this.status_code = null this.reason = null this._read_so_far = 0 this.headers = {} } /** * Close, delete and collect the response data */ public close() { if (this.socket) { this.socket.close() this.socket = null } this._cached = null } /** * The HTTP content direct from the socket, as bytes */ get content() { // print("Content length:", content_length) if (this._cached === null && this.socket) { const content_length = parseInt(this.headers["content-length"]) || 0 this._cached = this.socket.read(content_length) this.socket.close() this.socket = null } // print("Buffer length:", len(self._cached)) return this._cached } /** * The HTTP content, encoded into a string according to the HTTP header encoding */ get text() { const b = this.content; return b ? b.toString() : undefined; } get json() { return JSON.parse(this.text) } public toString() { return `HTTP ${this.status_code}; ${Object.keys(this.headers).length} headers; ${this._cached ? this._cached.length : -1} bytes content` } } export interface RequestOptions { data?: string | Buffer; json?: any; // will call JSON.stringify() headers?: StringMap; stream?: boolean; timeout?: number; // in ms } export function dataAsBuffer(data: string | Buffer): Buffer { if (data == null) return null if (typeof data == "string") return control.createBufferFromUTF8(data) return data } /* >>> "a,b,c,d,e".split(",", 2) ['a', 'b', 'c,d,e'] */ function pysplit(str: string, sep: string, limit: number) { const arr = str.split(sep) if (arr.length >= limit) { return arr.slice(0, limit).concat([arr.slice(limit).join(sep)]) } else { return arr } } /** Perform an HTTP request to the given url which we will parse to determine whether to use SSL ('https://') or not. We can also send some provided 'data' or a json dictionary which we will stringify. 'headers' is optional HTTP headers sent along. 'stream' will determine if we buffer everything, or whether to only read only when requested */ export function request(method: string, url: string, options?: RequestOptions): net.Response { net.log(`${method} ${url}`); if (!net.instance().controller) { // no controller const r = new net.Response(null); r.status_code = 418; // teapot r.reason = "net controller not configured"; return r; } try { return internalRequest(method, url, options); } catch (e) { const r = new net.Response(null); r.status_code = 418; // teapot r.reason = "" + e; return r; } } function internalRequest(method: string, url: string, options?: RequestOptions): net.Response { if (!options) options = {}; if (!options.headers) { options.headers = {} } const tmp = pysplit(url, "/", 3) let proto = tmp[0] let host = tmp[2] let path = tmp[3] || "" // replace spaces in path // TODO // path = path.replace(" ", "%20") let port = 0 if (proto == "http:") { port = 80 } else if (proto == "https:") { port = 443 } else { control.fail("Unsupported protocol: " + proto) } if (host.indexOf(":") >= 0) { const tmp = host.split(":") host = tmp[0] port = parseInt(tmp[1]) } let sock: Socket; if (proto == "https:") { // for SSL we need to know the host name sock = net.instance().createSocket(host, port, true) } else { sock = net.instance().createSocket(host, port, false) } // our response let resp = new Response(sock) // socket read timeout sock.setTimeout(options.timeout) sock.connect(); sock.send(`${method} /${path} HTTP/1.0\r\n`) if (!options.headers["Host"]) sock.send(`Host: ${host}\r\n`) if (!options.headers["User-Agent"]) sock.send("User-Agent: MakeCode ESP32\r\n") // Iterate over keys to avoid tuple alloc for (let k of Object.keys(options.headers)) sock.send(`${k}: ${options.headers[k]}\r\n`) if (options.json != null) { control.assert(options.data == null, 100) options.data = JSON.stringify(options.json) sock.send("Content-Type: application/json\r\n") } let dataBuf = dataAsBuffer(options.data) if (dataBuf) sock.send(`Content-Length: ${dataBuf.length}\r\n`) sock.send("\r\n") if (dataBuf) sock.send(dataBuf) let line = sock.readLine() // print(line) let line2 = pysplit(line, " ", 2) let status = parseInt(line2[1]) let reason = "" if (line2.length > 2) { reason = line2[2] } while (true) { line = sock.readLine() if (!line || line == "\r\n") { break } // print("**line: ", line) const tmp = pysplit(line, ": ", 1) let title = tmp[0] let content = tmp[1] if (title && content) { resp.headers[title.toLowerCase()] = content.toLowerCase() } } /* elif line.startswith(b"Location:") and not 200 <= status <= 299: raise NotImplementedError("Redirects not yet supported") */ if ((resp.headers["transfer-encoding"] || "").indexOf("chunked") >= 0) control.fail("not supported chunked encoding") resp.status_code = status resp.reason = reason return resp } /** * Send HTTP HEAD request **/ export function head(url: string, options?: RequestOptions) { return request("HEAD", url, options) } /** * Send HTTP GET request **/ export function get(url: string, options?: RequestOptions) { return request("GET", url, options) } /** * Send HTTP GET request and return text **/ //% blockId=netgetstring block="get string $url" export function getString(url: string, options?: RequestOptions): string { const res = get(url, options) const rv = res.status_code == 200 ? res.text : undefined res.close() return rv } /** * Send HTTP GET request and return JSON **/ //% blockId=netgetjson block="get json $url" export function getJSON(url: string, options?: RequestOptions): any { options = options || {}; options.headers = options.headers || {}; options.headers["accept"] = options.headers["accept"] || "application/json"; const res = get(url, options); const rv = res.status_code == 200 ? res.json : undefined; res.close() return rv } /** Send HTTP POST request */ export function post(url: string, options?: RequestOptions) { return request("POST", url, options) } /** Send HTTP PATCH request */ export function patch(url: string, options?: RequestOptions) { return request("PATCH", url, options) } /** Send HTTP PUT request */ export function put(url: string, options?: RequestOptions) { return request("PUT", url, options) } /** Send HTTP DELETE request */ export function del(url: string, options?: RequestOptions) { return request("DELETE", url, options) } }