sintexplicabo
Version:
520 lines (480 loc) • 16.8 kB
text/typescript
/***
* RestHttp(s) v1.0.0
*
* @write : Maroder
* @date : 2020-10-09
*
* Not3 :
* Basic send http(s) query, these object that allow to mount a http query doesn't check integrity of data sending,
* its just a wrap around the different options supported by node http(s). for the version 1.0.x only most popular
* options are supported for wrapping data, SSL/TLS options are supported. Some check are making when you have ending
* to informing the different options of the query with the `build` and `post` methods will make a minimal check of :
* + Made a concat with hostname + query_params variable
* + Will set "multipart/form-data" at Content-Type header property if this one is not defined.
* + Proxy replace path and hostname
*
* much things staying to do ... but I make sure that a basic request will be made,
* I tried to respect the typescript and object dev rules as much as possible, like Java IORestAssured
*
* exportable :
* - HttpOptions<T> : T is ResHttp or ResHttps object
* - RestHttp(s) : builder a new object with static constructor handler `options`
* - Response : Getter class
*
* All errors message are redirecting to Response Object.
*/
import * as http from "http";
import * as https from "https";
import {JSONException} from '../Exception';
import {List, loader, restHttp, streamLambdaTo, wrapHeader} from "../Interface";
import {ArrayList, HashMap} from "../List";
import {Proxy} from "./Proxy";
import {Cookie} from "./Cookie";
import "../globalUtils"
import {Define} from "../Define";
import {Constructor} from "../Constructor";
/**
*
*/
export type httpProtoType = "https"|"http"
export type httpMethodType = "HEAD"|"GET"|"PUT"|"POST"|"DELETE"|"CONNECT"|"TRACE"|"PATCH";
export type httpMimeType = "text/plain"|"text/html"|"text/xml"|"text/csv"|"application/octet-stream"|"application/xml"|
"application/json"|"application/javascript"|"multipart/form-data"|"application/x-www-form-urlencoded"|
"application/xhtml+xml"|"application/ld+json" //...
/***
* I keep this static class in private for this moment,
* but... it is only used to send requests
*/
class httpEvent{
/****
* + May be make a try/catch wrap on 'handle' variable, in order
* to return a RuntimeException when an error is throwing.
*
* @param rest
*/
public static async send( rest : AbstractRestHttp ):Promise<Response>{
let httpA : any = (rest.getProto().equals("http")?http:https),
handle;
return new Promise<Response>((resolve,reject)=>{
handle = httpA.request(
rest.getHeaderAsObject().toJson(),
response=>{
let chunk = [];
if(rest.getLoader()&&response.statusCode!==200)rest.getLoader().error(`Failed to download resources, status code : ${response.statusCode}`);
else if(rest.getLoader()&&response.statusCode===200) {
let s:number = parseInt(new Response({headers: response.headers}).getHeader("content-length"))||0;
rest.getLoader().setSizeOf(s);
}
response.on("data",data=>{ chunk.push(data); if(rest.getLoader()&&response.statusCode===200)rest.getLoader().add(data.length); });
response.on("end",()=>{httpEvent.pack(resolve,response,Buffer.concat(chunk).toString(rest.getEncoding()));});
}).on("error",error=>{
reject(new Response({ errorno:1,error:error }));
});
if(rest.getData()){
try{ handle.write(rest.getData());}catch (e) {
reject(new Response( { errorno:1,error:e }));
}
}
handle.end();
});
}
/***
* Repack response data
* @param resolve
* @param response
* @param body
*/
private static pack( resolve : Function, response : any, body : string ): void{
resolve(new Response({
errorno:0,error:null,statusMessage: response.statusMessage,
statusCode:response.statusCode, headers: response.headers, body: body
}));
}
}
/***
* RESPONSE CLASS
* Wrap getter http response
*/
export class Response {
/***
*
*/
private response : any = null;
/***
* @param response
*/
constructor( response : any ) { this.response = response;}
/***
*
*/
public getCookies() : ArrayList<Cookie> {
let mapToCookie : streamLambdaTo<string,Cookie>=value=>Cookie.parse(value);
return ArrayList.of<string>( this.getHeader("set-cookie") )
.stream()
.mapTo(mapToCookie)
.getList();
}
/***
*
*/
public getCookie( name :string ) : Cookie {
return this.getCookies()
.stream()
.filter(cookie=>cookie.getValue().equals(name))
.findFirst()
.orElse(null);
}
/***
*
*/
public getHeaders() : HashMap<string,any>{ return HashMap.of<string,any>(this.response.headers); }
/***
*
*/
public getHeader( name :string) : any{
try{return this.response.headers[name]}catch (e) {
return null;
}
}
/***
*
*/
public getBody():string{ return this.response.body;}
/***
* throwable : RuntimeException
*
*/
public getBodyAsObject():any{
try{return JSON.parse(this.response.body);}catch (e) {
throw new JSONException(e);
}
}
/***
*
*/
public getStatusCode( ): Number{ return Number(this.response.statusCode); }
/***
*
*/
public getStatusMessage( ): string{ return this.response.statusMessage; }
/***
*
*/
public contentType() : string{ return this.getHeader("Content-Type")||this.getHeader("content-type"); }
/***
*
*/
public hasError( ): boolean{ return this.response.errorno>0; }
/***
*
*/
public getLasError( ): string{ return this.response.error; }
/***
*
* @param code
*/
public success( code : number ): Define<Response>{
return new Define<Response>( this.getStatusCode().equals(code) ? this : null );
}
}
/***
*
*/
abstract class AbstractRestHttp implements restHttp{
/***
*
*/
protected encoding:BufferEncoding = "utf-8";
protected proto :httpProtoType = null;
protected header :HashMap<string,any> = null;
protected data :string = null;
private loader:loader = null;
/***
*
*/
public getProto( ): string{ return String(this.proto);}
/***
*
*/
public getHeaderAsObject( ) : HashMap<string,any> {return this.header}
/***
*
*/
public getDataAsObject( ) :any{
try{return JSON.parse(this.data);}catch (e) {
return null;
}
}
/***
*
*/
public getData( ): string { return this.data;}
/***
* @async
*/
public async request( ) : Promise<Response>{
try{
if(this.loader) this.loader.start("Download was successful !");
return await httpEvent.send(this);
} catch (Exception) {
return Exception;
}
}
public setData(data: string): void { this.data = data; }
public setHeader(header: HashMap<string, any>): void { this.header = header}
public setEncoding( encoding:BufferEncoding):restHttp{this.encoding = encoding; return this;}
public getEncoding( ):BufferEncoding{ return this.encoding;}
public setLoader( pipe:loader): restHttp { this.loader = pipe; return this; }
public getLoader( ): loader{ return this.loader; }
}
export class RestHttp extends AbstractRestHttp{
/***
*
*/
protected proto : httpProtoType = "http";
/***
* @param header
* @param data
*/
constructor( header : HashMap<string,any> = null, data : string = null) {
super();
this.header = header;
this.data = data;
}
/***
* Constructor
*/
public static options( ) : HttpOptions<RestHttp>{
return new HttpOptions<RestHttp>( RestHttp );
}
}
export class RestHttps extends RestHttp{
/***
*
*/
protected proto : httpProtoType = "https";
/***
* @param header
* @param data
*/
constructor(header : HashMap<string,any>, data : string) {super(header,data); }
/***
* Constructor
*/
public static options( ) : HttpOptions<RestHttps>{
return new HttpOptions<RestHttps>( RestHttps );
}
}
/***
* Only most popular options are implemented,
* you can add new options yourself it not prohibited.
*/
export class HttpOptions<T extends restHttp> implements wrapHeader<T>{
private options : HashMap<string,any> = new HashMap<string,any>({});
private data : object|string = null;
private params : string = "";
private Class : Constructor<T>;
constructor( value : Function ){
this.Class = value.class();
this.options.put("headers",new HashMap<string,string>({}));
this.withEndPoint("/");
this.withPort(this.Class.newInstance().getProto().equals("https") ? 443 : 80 );
}
/***
*
*/
public withHostname( hostname : string ) : HttpOptions<T>{
this.options.put("hostname",hostname);
return this;
}
public withPort( port : number): HttpOptions<T>{
this.options.put("port",port);
return this;
}
public withEndPoint(endPoint : string): HttpOptions<T>{
this.options.put("path",endPoint);
return this;
}
public withTimeout( timeout : number ): HttpOptions<T>{
this.options.put("timeout",timeout);
return this;
}
/***
* Method Area
*/
public withMethod(method : httpMethodType ): HttpOptions<T>{
this.options.put("method",method);
return this;
}
public get( ) : HttpOptions<T>{return this.withMethod("GET");}
public put( ) : HttpOptions<T>{return this.withMethod("PUT");}
public delete( ) : HttpOptions<T>{return this.withMethod("DELETE");}
public post( ) : HttpOptions<T>{
if( !this.options.get("headers").get("Content-Type") )this.widthContentType("multipart/form-data");
return this.withMethod("POST");
}
public withInsecureHTTPParser( state : boolean ) : HttpOptions<T>{
this.options.put("insecureHTTPParser",state);
return this;
}
/***
* Headers Are
*/
public withHeader(type :string, value : any ): HttpOptions<T>{
this.options.get("headers").put( type, value);
return this;
}
public widthContentType( mime : httpMimeType|string ): HttpOptions<T> {
return this.withHeader("Content-Type",mime);
}
public widthAccept( mime : httpMimeType|string ): HttpOptions<T> {
return this.withHeader("Accept", mime );
}
public widthJson( mime : httpMimeType|string ): HttpOptions<T> {
this.withHeader("Accept", "application/json" );
return this.widthContentType("application/json" );
}
/***
* Auth Area
*/
public withAuth( auth : string ): HttpOptions<T>{
this.options.put("auth",auth);
return this;
}
/***
* SSL/TLS options
*
*/
public withRejectUnauthorized( state : boolean ): HttpOptions<T>{
this.options.put("rejectUnauthorized",state);
return this;
}
public withRequestCert( state : boolean ): HttpOptions<T> {
this.options.put("requestCert",state);
return this;
}
public withPfx( pfx : Buffer|string ): HttpOptions<T>{
this.options.put("pfx",pfx);
return this;
}
public withPassPhrase( passphrase : string ): HttpOptions<T>{
this.options.put("passphrase",passphrase);
return this;
}
public withCACert( cacert : Buffer|string ): HttpOptions<T>{
this.options.put("cacert",cacert);
return this;
}
public withCert( cert : Buffer|string ): HttpOptions<T>{
this.options.put("cert",cert);
return this;
}
public withDhparam( dhparam : string ): HttpOptions<T>{
this.options.put("dhparam",dhparam);
return this;
}
public withCrl( crl : Buffer|string ): HttpOptions<T>{
this.options.put("crl",crl);
return this;
}
public withSSlKey( key : string ): HttpOptions<T>{
this.options.put("key",key);
return this;
}
public withCiphers( ciphers : Array<String> ) : HttpOptions<T>{
this.options.put("ciphers",ciphers.join(":"));
return this;
}
public withSessionTimeout( sessionTimeout : number ) : HttpOptions<T>{
this.options.put("sessionTimeout",sessionTimeout);
return this;
}
public withSecureOptions( value : number ): HttpOptions<T>{
this.options.put("secureOptions",value);
return this;
}
public withSigalgs( value : string ): HttpOptions<T>{
this.options.put("sigalgs",value);
return this;
}
/***
* @withProxy :
* httpProxy has [http_proxy_port] -> [http_proxy_port] who reflect => 80
* httpsProxy has [https_proxy_port] -> [http_proxy_port] who reflect => 443
* now query :
* httpProxy -> with httpsQuery -> gotta reverse redirect protocol, otherwise follow same proto than proxy
* port has defined in the redirect url : port !== ( [http_proxy_port] & 80 )
*
* + can be an reverse redirect protocol or simply an another server with no https,
* just a another service hosted under a other port than 80 or [http_proxy_port].
*
* httpsProxy : same than httpProxy
* @param proxy
* @param noReverseProtocolFor
*/
public withProxy( proxy : Proxy, noReverseProtocolFor: List<number> = new ArrayList() ) : HttpOptions<T>{
let url:string = proxy.getHttpProxy(),
host:string = this.options.get("hostname")||this.options.get("host"),
proto:string = this.Class.newInstance().getProto(),
originalPort:number = <number>proto.equals("http").state(80,443),
port:number, tmp:List<string>;
if(proto.equals("https")) url = proxy.getHttpsProxy();
// define proxy port => url:port
if((tmp=url.explodeAsList(":")).size().equals(2)){
port = Number( tmp.get(1).orDefault(proto.equals("https").state("8443","8080")) );
url = tmp.get(0);
}else // no port defined may be properties is defined withPort method
// otherwise default port 8080 & 8443
port = Define.of<number>(this.options.get("port"))
.orNull(proto.equals("http").state(8080,8443))
if((tmp=host.explodeAsList(/:(\d+)$/)).size().equals(3)){
if(!Number(tmp.get(1)).equals(originalPort)&&noReverseProtocolFor.indexOf(<Object>Number(tmp.get(1))).equals(-1))
proto = proto.equals("http")?"https":"http";
}
return this.withEndPoint(proto+"://"+host+this.options.get("path"))
.withPort( port )
.withHeader("Referer",this.options.get("path"))
.withHeader("Host",host)
.withHostname(url);
}
/***
*
* @param params
*/
private packParams( params : object ): string{
let chunk="";
HashMap.of<string,any>(params).each((value,key)=>chunk += "%s=%s&".format(key,encodeURI(String(value))));
return chunk.replace(/\&$/,"");
}
public withPostData( data : object|string ): HttpOptions<T>{
return this.withData(data,true);
}
/***
* Method throwable : JSON.Parse
* @param data
* @param post
*/
public withData(data : object|string, post: boolean = false ): HttpOptions<T>{
let dat="";
switch (typeof data) {
case "object": dat = post? this.packParams(data):JSON.stringify(data); break;
case "string": dat = data; break;
}
this.data = data;
return this.withHeader("Content-Length",dat.length);
}
/**
* @param params
*/
public withParams( params : object ): HttpOptions<T>{
this.params = "%s%s".format("?",this.packParams(params));
return this;
}
/***
* @constructor
*/
public build() : T{
if(!this.params.isEmpty()) this.options.put("path", (this.options.get("path")||"/")+this.params );
return this.Class.newInstance(this.options,JSON.stringify(this.data));
}
}