blob: 63fdc04f369a60d38b8bd53b7d3566e8846942ef [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import groovy.json.JsonOutput
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import java.security.SecureRandom;
import javax.net.ssl.HttpsURLConnection;
/**
* simple http rest client for groovy
* by dlukyanov@ukr.net
*/
@groovy.transform.CompileStatic
public class HTTP{
//default response handler
public static Closure TEXT_RECEIVER = {InputStream instr,Map ctx->
return instr.getText( (String)ctx.encoding );
}
public static Closure JSON_RECEIVER = { InputStream instr, Map ctx->
return new groovy.json.JsonSlurper().parse(instr,(String)ctx.encoding);
}
public static Closure FILE_RECEIVER(File f){
return { InputStream instr, Map ctx->
f<<instr;
return f;
}
}
public static Map<String,Object> get(Map<String,Object> ctx)throws IOException{
ctx.put('method','GET');
return send(ctx);
}
public static Map<String,Object> head(Map<String,Object> ctx)throws IOException{
ctx.put('method','HEAD');
return send(ctx);
}
public static Map<String,Object> post(Map<String,Object> ctx)throws IOException{
ctx.put('method','POST');
return send(ctx);
}
public static Map<String,Object> put(Map<String,Object> ctx)throws IOException{
ctx.put('method','PUT');
return send(ctx);
}
public static Map<String,Object> delete(Map<String,Object> ctx)throws IOException{
ctx.put('method','DELETE');
return send(ctx);
}
/**
* @param url string where to send request
* @param query Map parameters to append to url
* @param method http method to be used in request. standard methods: GET, POST, PUT, DELETE, HEAD
* @param headers key-value map with headers that should be sent with request
* @param body request body/data to send to url (InputStream, CharSequence, or Map for json and x-www-form-urlencoded context types)
* @param encoding encoding name to use to send/receive data - default UTF-8
* @param receiver Closure that will be called to receive data from server. Defaults: `HTTP.JSON_RECEIVER` for json content-type and `HTTP.TEXT_RECEIVER` otherwise. Available: `HTTP.FILE_RECEIVER(File)` - stores response to file.
* @param ssl javax.net.ssl.SSLContext or String that evaluates the javax.net.ssl.SSLContext. example: send( url:..., ssl: "HTTP.getKeystoreSSLContext('./keystore.jks', 'testpass')" )
*/
public static Map<String,Object> send(Map<String,Object> ctx)throws IOException{
String url = ctx.url;
Map<String,String> headers = (Map<String,String>)ctx.headers;
String method = ctx.method;
Object body = ctx.body;
String encoding = ctx.encoding?:"UTF-8";
Closure receiver = (Closure)ctx.receiver;
Map<String,String> query = (Map<String,String>)ctx.query;
Object sslCtxObj= ctx.ssl;
//copy context and set default values
ctx = [:] + ctx;
ctx.encoding = encoding;
String contentType="";
if(query){
url+="?"+query.collect{k,v-> k+"="+URLEncoder.encode(v,'UTF-8') }.join('&')
}
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
if(sslCtxObj!=null && connection instanceof HttpsURLConnection){
SSLContext sslCtx = null;
if(sslCtxObj instanceof SSLContext){
sslCtx = (SSLContext)sslCtxObj;
}else if(sslCtxObj instanceof CharSequence){
//assume this is a groovy code to get ssl context
sslCtx = evaluateSSLContext((CharSequence)sslCtxObj);
}else{
throw new IllegalArgumentException("Unsupported ssl parameter ${sslCtxObj.getClass()}")
}
((HttpsURLConnection)connection).setSSLSocketFactory(sslCtx.getSocketFactory());
}
connection.setDoOutput(true);
connection.setRequestMethod(method);
if ( headers!=null && !headers.isEmpty() ) {
//add headers
for (Map.Entry<String, String> entry : headers.entrySet()) {
if(entry.getValue()){
connection.addRequestProperty(entry.getKey(), entry.getValue());
if("content-type".equals(entry.getKey().toLowerCase()))contentType=entry.getValue();
}
}
}
if(body!=null){
//write body
OutputStream out = connection.getOutputStream();
if( body instanceof Closure ){
((Closure)body).call(out, ctx);
}else if(body instanceof InputStream){
out << (InputStream)body;
}else if(body instanceof Map){
if( contentType =~ "(?i)[^/]+/json" ) {
out.withWriter((String)ctx.encoding){
it.append( JsonOutput.toJson((Map)body) );
}
} else if( contentType =~ "(?i)[^/]+/x-www-form-urlencoded" ) {
out.withWriter((String)ctx.encoding) {
it.append( ((Map)body).collect{k,v-> ""+k+"="+URLEncoder.encode((String)v,'UTF-8') }.join('&') )
}
} else {
throw new IOException("Map body type supported only for */json of */x-www-form-urlencoded content-type");
}
}else if(body instanceof CharSequence){
out.withWriter((String)ctx.encoding){
it.append((CharSequence)body);
}
}else{
throw new IOException("Unsupported body type: "+body.getClass());
}
out.flush();
out.close();
out=null;
}
Map response = [:];
ctx.response = response;
response.code = connection.getResponseCode();
response.message = connection.getResponseMessage();
response.headers = connection.getHeaderFields();
InputStream instr = null;
if( ((int)response.code)>=400 ){
try{
instr = connection.getErrorStream();
}catch(Exception ei){}
}else{
try{
instr = connection.getInputStream();
}catch(java.io.IOException ei){
throw new IOException("fail to open InputStream for http code "+response.code+":"+ei);
}
}
if(instr!=null) {
instr = new BufferedInputStream(instr);
if(receiver==null){
if( response.headers['Content-Type']?.toString()?.indexOf('/json')>0 ){
receiver=JSON_RECEIVER;
} else receiver=TEXT_RECEIVER;
}
response.body = receiver(instr,ctx);
instr.close();
instr=null;
}
return ctx;
}
@groovy.transform.Memoized
public static SSLContext getKeystoreSSLContext(String keystorePath, String keystorePass, String keystoreType="JKS", String keyPass = null){
if(keyPass == null) keyPass=keystorePass;
KeyStore clientStore = KeyStore.getInstance(keystoreType);
clientStore.load(new File( keystorePath ).newInputStream(), keystorePass.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientStore, keyPass.toCharArray());
KeyManager[] kms = kmf.getKeyManagers();
//init TrustCerts
TrustManager[] trustCerts = new TrustManager[1];
trustCerts[0] = new X509TrustManager() {
public void checkClientTrusted( final X509Certificate[] chain, final String authType ) { }
public void checkServerTrusted( final X509Certificate[] chain, final String authType ) { }
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kms, trustCerts, new SecureRandom());
return sslContext;
}
@groovy.transform.Memoized
public static SSLContext getNaiveSSLContext(){
System.err.println("HTTP.getNaiveSSLContext() used. Must be disabled on prod!");
KeyManager[] kms = new KeyManager[0];
TrustManager[] trustCerts = new TrustManager[1];
trustCerts[0] = new X509TrustManager() {
public void checkClientTrusted( final X509Certificate[] chain, final String authType ) { }
public void checkServerTrusted( final X509Certificate[] chain, final String authType ) { }
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustCerts, new SecureRandom());
return sslContext;
}
/**
* evaluates code that should return SSLContext
*/
@groovy.transform.Memoized
public static SSLContext evaluateSSLContext(CharSequence code) {
Object ssl = new GroovyShell( HTTP.class.getClassLoader() ).evaluate( code as String );
return (SSLContext) ssl;
}
}