From 5bf9f9e3c5ae56e7fb4bcac3efbaaf7eb7d58094 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Tue, 15 May 2018 10:10:23 +0100 Subject: [PATCH] Allow multiple HTTP request methods This implements an argument format similar to LuaReqeust, as described in dan200/ComputerCraft#515. The Lua argument checking code is a little verbose and repetitive, but I'm not sure how to avoid that - we should look into improving it in the future. Closes #21 --- .../computercraft/core/apis/HTTPAPI.java | 80 ++++--- .../computercraft/core/apis/TableHelper.java | 214 ++++++++++++++++++ .../core/apis/http/HTTPRequest.java | 8 +- .../assets/computercraft/lua/bios.lua | 79 +++++-- 4 files changed, 335 insertions(+), 46 deletions(-) create mode 100644 src/main/java/dan200/computercraft/core/apis/TableHelper.java diff --git a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java index bd283f73e..2f797e3d9 100644 --- a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -6,6 +6,7 @@ package dan200.computercraft.core.apis; +import com.google.common.collect.ImmutableSet; import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.ILuaContext; @@ -21,9 +22,14 @@ import java.util.*; import java.util.concurrent.Future; import static dan200.computercraft.core.apis.ArgumentHelper.*; +import static dan200.computercraft.core.apis.TableHelper.*; public class HTTPAPI implements ILuaAPI { + private static final Set HTTP_METHODS = ImmutableSet.of( + "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE" + ); + private final IAPIEnvironment m_apiEnvironment; private final List> m_httpTasks; private final Set m_closeables; @@ -38,7 +44,7 @@ public class HTTPAPI implements ILuaAPI @Override public String[] getNames() { - return new String[] { + return new String[]{ "http" }; } @@ -59,7 +65,7 @@ public class HTTPAPI implements ILuaAPI } @Override - public void shutdown( ) + public void shutdown() { synchronized( m_httpTasks ) { @@ -89,7 +95,7 @@ public class HTTPAPI implements ILuaAPI @Override public String[] getMethodNames() { - return new String[] { + return new String[]{ "request", "checkURL", "websocket", @@ -101,52 +107,68 @@ public class HTTPAPI implements ILuaAPI { switch( method ) { - case 0: + case 0: // request { - // request - // Get URL - String urlString = getString( args, 0 ); + String urlString, postString, requestMethod; + Map headerTable; + boolean binary, redirect; - // Get POST - String postString = optString( args, 1, null ); - - // Get Headers - Map headers = null; - Map table = optTable( args, 2, null ); - if( table != null ) + if( args.length >= 1 && args[0] instanceof Map ) { - headers = new HashMap<>( table.size() ); - for( Object key : table.keySet() ) + Map options = (Map) args[0]; + urlString = getStringField( options, "url" ); + postString = optStringField( options, "body", null ); + headerTable = optTableField( options, "headers", null ); + binary = optBooleanField( options, "binary", false ); + requestMethod = optStringField( options, "method", null ); + redirect = optBooleanField( options, "redirect", true ); + + } + else + { + // Get URL and post information + urlString = getString( args, 0 ); + postString = optString( args, 1, null ); + headerTable = optTable( args, 2, null ); + binary = optBoolean( args, 3, false ); + requestMethod = null; + redirect = true; + } + + Map headers = null; + if( headerTable != null ) + { + headers = new HashMap<>( headerTable.size() ); + for( Object key : headerTable.keySet() ) { - Object value = table.get( key ); + Object value = headerTable.get( key ); if( key instanceof String && value instanceof String ) { - headers.put( (String)key, (String)value ); + headers.put( (String) key, (String) value ); } } } - // Get binary - boolean binary = false; - if( args.length >= 4 ) + + if( requestMethod != null && !HTTP_METHODS.contains( requestMethod ) ) { - binary = args[ 3 ] != null && !args[ 3 ].equals( Boolean.FALSE ); + throw new LuaException( "Unsupported HTTP method" ); } // Make the request try { URL url = HTTPRequest.checkURL( urlString ); - HTTPRequest request = new HTTPRequest( m_apiEnvironment, urlString, url, postString, headers, binary ); + HTTPRequest request = new HTTPRequest( m_apiEnvironment, urlString, url, postString, headers, binary, requestMethod, redirect ); synchronized( m_httpTasks ) { m_httpTasks.add( HTTPExecutor.EXECUTOR.submit( request ) ); } - return new Object[] { true }; + return new Object[]{ true }; } catch( HTTPRequestException e ) { - return new Object[] { false, e.getMessage() }; + return new Object[]{ false, e.getMessage() }; } } case 1: @@ -164,11 +186,11 @@ public class HTTPAPI implements ILuaAPI { m_httpTasks.add( HTTPExecutor.EXECUTOR.submit( check ) ); } - return new Object[] { true }; + return new Object[]{ true }; } catch( HTTPRequestException e ) { - return new Object[] { false, e.getMessage() }; + return new Object[]{ false, e.getMessage() }; } } case 2: // websocket @@ -201,11 +223,11 @@ public class HTTPAPI implements ILuaAPI { m_httpTasks.add( connector ); } - return new Object[] { true }; + return new Object[]{ true }; } catch( HTTPRequestException e ) { - return new Object[] { false, e.getMessage() }; + return new Object[]{ false, e.getMessage() }; } } default: diff --git a/src/main/java/dan200/computercraft/core/apis/TableHelper.java b/src/main/java/dan200/computercraft/core/apis/TableHelper.java new file mode 100644 index 000000000..1d62be076 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/TableHelper.java @@ -0,0 +1,214 @@ +package dan200.computercraft.core.apis; + +import dan200.computercraft.api.lua.LuaException; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; + +/** + * Various helpers for tables + */ +public final class TableHelper +{ + private TableHelper() + { + throw new IllegalStateException( "Cannot instantiate singleton " + getClass().getName() ); + } + + @Nonnull + public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nullable Object actual ) + { + return badKey( key, expected, ArgumentHelper.getType( actual ) ); + } + + @Nonnull + public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nonnull String actual ) + { + return new LuaException( "bad field '" + key + "' (" + expected + " expected, got " + actual + ")" ); + } + + public static double getNumberField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof Number ) + { + return ((Number) value).doubleValue(); + } + else + { + throw badKey( key, "number", value ); + } + } + + public static int getIntField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof Number ) + { + return (int) ((Number) value).longValue(); + } + else + { + throw badKey( key, "number", value ); + } + } + + public static double getRealField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + return checkReal( key, getNumberField( table, key ) ); + } + + public static boolean getBooleanField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof Boolean ) + { + return (Boolean) value; + } + else + { + throw badKey( key, "boolean", value ); + } + } + + @Nonnull + public static String getStringField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof String ) + { + return (String) value; + } + else + { + throw badKey( key, "string", value ); + } + } + + @SuppressWarnings( "unchecked" ) + @Nonnull + public static Map getTableField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof Map ) + { + return (Map) value; + } + else + { + throw badKey( key, "table", value ); + } + } + + public static double optNumberField( @Nonnull Map table, @Nonnull String key, double def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof Number ) + { + return ((Number) value).doubleValue(); + } + else + { + throw badKey( key, "number", value ); + } + } + + public static int optIntField( @Nonnull Map table, @Nonnull String key, int def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof Number ) + { + return (int) ((Number) value).longValue(); + } + else + { + throw badKey( key, "number", value ); + } + } + + public static double optRealField( @Nonnull Map table, @Nonnull String key, double def ) throws LuaException + { + return checkReal( key, optNumberField( table, key, def ) ); + } + + public static boolean optBooleanField( @Nonnull Map table, @Nonnull String key, boolean def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof Boolean ) + { + return (Boolean) value; + } + else + { + throw badKey( key, "boolean", value ); + } + } + + public static String optStringField( @Nonnull Map table, @Nonnull String key, String def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof String ) + { + return (String) value; + } + else + { + throw badKey( key, "string", value ); + } + } + + @SuppressWarnings( "unchecked" ) + public static Map optTableField( @Nonnull Map table, @Nonnull String key, Map def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof Map ) + { + return (Map) value; + } + else + { + throw badKey( key, "table", value ); + } + } + + private static double checkReal( @Nonnull String key, double value ) throws LuaException + { + if( Double.isNaN( value ) ) + { + throw badKey( key, "number", "nan" ); + } + else if( value == Double.POSITIVE_INFINITY ) + { + throw badKey( key, "number", "inf" ); + } + else if( value == Double.NEGATIVE_INFINITY ) + { + throw badKey( key, "number", "-inf" ); + } + else + { + return value; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java b/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java index 93b0b96a5..0ed3206fd 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java +++ b/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java @@ -79,8 +79,10 @@ public class HTTPRequest implements Runnable private final String m_postText; private final Map m_headers; private boolean m_binary; + private final String m_method; + private final boolean m_followRedirects; - public HTTPRequest( IAPIEnvironment environment, String urlString, URL url, final String postText, final Map headers, boolean binary ) throws HTTPRequestException + public HTTPRequest( IAPIEnvironment environment, String urlString, URL url, final String postText, final Map headers, boolean binary, final String method, final boolean followRedirects ) throws HTTPRequestException { m_environment = environment; m_urlString = urlString; @@ -88,6 +90,8 @@ public class HTTPRequest implements Runnable m_binary = binary; m_postText = postText; m_headers = headers; + m_method = method; + m_followRedirects = followRedirects; } @Override @@ -120,6 +124,8 @@ public class HTTPRequest implements Runnable { connection.setRequestMethod( "GET" ); } + if( m_method != null ) connection.setRequestMethod( m_method ); + connection.setInstanceFollowRedirects( m_followRedirects ); // Set headers connection.setRequestProperty( "accept-charset", "UTF-8" ); diff --git a/src/main/resources/assets/computercraft/lua/bios.lua b/src/main/resources/assets/computercraft/lua/bios.lua index 6d9b7885a..8e9703da0 100644 --- a/src/main/resources/assets/computercraft/lua/bios.lua +++ b/src/main/resources/assets/computercraft/lua/bios.lua @@ -662,8 +662,36 @@ end if http then local nativeHTTPRequest = http.request - local function wrapRequest( _url, _post, _headers, _binary ) - local ok, err = nativeHTTPRequest( _url, _post, _headers, _binary ) + local methods = { + GET = true, POST = true, HEAD = true, + OPTIONS = true, PUT = true, DELETE = true + } + + local function checkKey( options, key, ty, opt ) + local value = options[key] + local valueTy = type(value) + + if (value ~= nil or not opt) and valueTy ~= ty then + error(("bad field '%s' (expected %s, got %s"):format(key, ty, valueTy), 4) + end + end + + local function checkOptions( options, body ) + checkKey( options, "url", "string") + if body == false + then checkKey( options, "body", "nil" ) + else checkKey( options, "body", "string", not body ) end + checkKey( options, "headers", "table", true ) + checkKey( options, "method", "string", true ) + checkKey( options, "redirect", "boolean", true ) + + if options.method and not methods[options.method] then + error( "Unsupported HTTP method", 3 ) + end + end + + local function wrapRequest( _url, ... ) + local ok, err = nativeHTTPRequest( ... ) if ok then while true do local event, param1, param2, param3 = os.pullEvent() @@ -678,6 +706,11 @@ if http then end http.get = function( _url, _headers, _binary) + if type( _url ) == "table" then + checkOptions( _url, false ) + return wrapRequest( _url.url, _url ) + end + if type( _url ) ~= "string" then error( "bad argument #1 (expected string, got " .. type( _url ) .. ")", 2 ) end @@ -687,10 +720,15 @@ if http then if _binary ~= nil and type( _binary ) ~= "boolean" then error( "bad argument #3 (expected boolean, got " .. type( _binary ) .. ")", 2 ) end - return wrapRequest( _url, nil, _headers, _binary) + return wrapRequest( _url, _url, nil, _headers, _binary ) end http.post = function( _url, _post, _headers, _binary) + if type( _url ) == "table" then + checkOptions( _url, true ) + return wrapRequest( _url.url, _url ) + end + if type( _url ) ~= "string" then error( "bad argument #1 (expected string, got " .. type( _url ) .. ")", 2 ) end @@ -703,25 +741,34 @@ if http then if _binary ~= nil and type( _binary ) ~= "boolean" then error( "bad argument #4 (expected boolean, got " .. type( _binary ) .. ")", 2 ) end - return wrapRequest( _url, _post or "", _headers, _binary) + return wrapRequest( _url, _url, _post, _headers, _binary ) end http.request = function( _url, _post, _headers, _binary ) - if type( _url ) ~= "string" then - error( "bad argument #1 (expected string, got " .. type( _url ) .. ")", 2 ) - end - if _post ~= nil and type( _post ) ~= "string" then - error( "bad argument #2 (expected string, got " .. type( _post ) .. ")", 2 ) - end - if _headers ~= nil and type( _headers ) ~= "table" then - error( "bad argument #3 (expected table, got " .. type( _headers ) .. ")", 2 ) - end - if _binary ~= nil and type( _binary ) ~= "boolean" then - error( "bad argument #4 (expected boolean, got " .. type( _binary ) .. ")", 2 ) + local url + if type( _url ) == "table" then + checkOptions( _url ) + url = _url.url + else + if type( _url ) ~= "string" then + error( "bad argument #1 (expected string, got " .. type( _url ) .. ")", 2 ) + end + if _post ~= nil and type( _post ) ~= "string" then + error( "bad argument #2 (expected string, got " .. type( _post ) .. ")", 2 ) + end + if _headers ~= nil and type( _headers ) ~= "table" then + error( "bad argument #3 (expected table, got " .. type( _headers ) .. ")", 2 ) + end + if _binary ~= nil and type( _binary ) ~= "boolean" then + error( "bad argument #4 (expected boolean, got " .. type( _binary ) .. ")", 2 ) + end + + url = _url.url end + local ok, err = nativeHTTPRequest( _url, _post, _headers, _binary ) if not ok then - os.queueEvent( "http_failure", _url, err ) + os.queueEvent( "http_failure", url, err ) end return ok, err end