pxt-common-packages
Version:
Microsoft MakeCode (PXT) common packages
306 lines (262 loc) • 8.99 kB
text/typescript
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)
}
}