From 71340da068bcdde2024a0f28f4937d0005afac1b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 19 Apr 2021 20:34:12 +1000 Subject: [PATCH] Improved GET requests - Uses custom HttpClient - Callback for HTTPS certificate errors - Major code refactor = cleaner code! - All response validation is performed in the API now! --- lib/api.dart | 226 ++++++++++++++++++++++++--------------- lib/inventree/model.dart | 119 ++------------------- 2 files changed, 151 insertions(+), 194 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 64d2664f..1dbdc191 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -165,44 +165,19 @@ class InvenTreeAPI { print("Connecting to ${apiUrl} -> ${username}:${password}"); - var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { - - print("Error connecting to server: ${error.toString()}"); - - if (error is SocketException) { - showServerError( - I18N.of(context).connectionRefused, - error.toString()); - return null; - } else if (error is TimeoutException) { - showTimeoutError(context); - return null; - } else { - // Unknown error type - re-throw - throw error; - } - }); + // Request the /api/ endpoint - response is a json object + var response = await get(""); + // Null response means something went horribly wrong! if (response == null) { - // Null (or error) response: Show dialog and exit return false; } - if (response.statusCode != 200) { - // Any status code other than 200! - showStatusCodeError(response.statusCode); - - // TODO: Interpret the error codes and show custom message? - return false; - } - - var data = json.decode(response.body); - - print("Response from server: $data"); + print("Response from server: ${response}"); // We expect certain response from the server - if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) { + if (!response.containsKey("server") || !response.containsKey("version") || !response.containsKey("instance")) { showServerError( "Missing Data", @@ -213,11 +188,11 @@ class InvenTreeAPI { } // Record server information - _version = data["version"]; - instance = data['instance'] ?? ''; + _version = response["version"]; + instance = response['instance'] ?? ''; // Default API version is 1 if not provided - _apiVersion = data['apiVersion'] as int ?? 1; + _apiVersion = response['apiVersion'] as int ?? 1; if (_apiVersion < _minApiVersion) { @@ -245,14 +220,7 @@ class InvenTreeAPI { print("Requesting token from server"); - response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) { - - print("Error requesting token:"); - print(error); - - response = null; - - }); + response = await get(_URL_GET_TOKEN); if (response == null) { showServerError( @@ -263,31 +231,25 @@ class InvenTreeAPI { return false; } - if (response.statusCode != 200) { - showStatusCodeError(response.statusCode); - return false; - } else { - var data = json.decode(response.body); - - if (!data.containsKey("token")) { - showServerError( + if (!response.containsKey("token")) { + showServerError( I18N.of(OneContext().context).tokenMissing, "Access token missing from response" - ); + ); - return false; - } + return false; + } - // Return the received token - _token = data["token"]; - print("Received token - $_token"); + // Return the received token + _token = response["token"]; + print("Received token - $_token"); - // Request user role information - await getUserRoles(); + // Request user role information + await getUserRoles(); + + // Ok, probably pretty good... + return true; - // Ok, probably pretty good... - return true; - }; } bool disconnectFromServer() { @@ -348,32 +310,19 @@ class InvenTreeAPI { // Note: 2021-02-27 this "roles" feature for the API was just introduced. // Any 'older' version of the server allows any API method for any logged in user! // We will return immediately, but request the user roles in the background - await get(_URL_GET_ROLES).timeout( - Duration(seconds: 10)).catchError((error) { - print("Error requesting roles:"); - print(error); - }).then((response) { - print("Response status: ${response.statusCode}"); + var response = await get(_URL_GET_ROLES); - if (response.statusCode == 200) { + // Null response from server + if (response == null) { + print("null response requesting user roles"); + return; + } - // Convert response to JSON representation - - try { - var data = json.decode(response.body); - - if (data.containsKey('roles')) { - // Save a local copy of the user roles - roles = data['roles']; - } - } - on FormatException { - // Old server has re-directed away from the API - } - } else { - } - }); + if (response.containsKey('roles')) { + // Save a local copy of the user roles + roles = response['roles']; + } } bool checkPermission(String role, String permission) { @@ -454,11 +403,42 @@ class InvenTreeAPI { ); } - // Perform a GET request - Future get(String url, {Map params}) async { + HttpClient _client(bool allowBadCert) { + + var client = new HttpClient(); + + client.badCertificateCallback = ((X509Certificate cert, String host, int port) { + // TODO - Introspection of actual certificate? + + if (allowBadCert) { + return true; + } else { + showServerError( + I18N.of(OneContext().context).serverCertificateError, + "Server HTTPS certificate invalid" + ); + return false; + } + + return allowBadCert; + }); + + // Set the connection timeout + client.connectionTimeout = Duration(seconds: 30); + + return client; + } + + /** + * Perform a HTTP GET request + * Returns a json object (or null if did not complete) + */ + Future get(String url, {Map params}) async { var _url = makeApiUrl(url); var _headers = defaultHeaders(); + print("GET: ${_url}"); + // If query parameters are supplied, form a query string if (params != null && params.isNotEmpty) { String query = '?'; @@ -475,7 +455,80 @@ class InvenTreeAPI { print("GET: " + _url); - return http.get(_url, headers: _headers); + var client = _client(true); + + print("Created client"); + + HttpClientRequest request = await client.getUrl(Uri.parse(_url)); + + // Set headers + request.headers.set(HttpHeaders.contentTypeHeader, 'application/json'); + if (profile != null) { + request.headers.set( + HttpHeaders.authorizationHeader, + _authorizationHeader(profile.username, profile.password) + ); + } + + print("Created request: ${request.uri}"); + + HttpClientResponse response = await request.close() + .timeout(Duration(seconds: 30)) + .catchError((error) { + print("GET request returned error"); + print("URL: ${_url}"); + print("Error: ${error.toString()}"); + + var ctx = OneContext().context; + + if (error is SocketException) { + showServerError( + I18N.of(ctx).connectionRefused, + error.toString() + ); + } else if (error is TimeoutException) { + showTimeoutError(ctx); + } else { + showServerError( + I18N.of(ctx).serverError, + error.toString() + ); + } + + return null; + }); + + // A null response means something has gone wrong... + if (response == null) { + print("null response from GET ${_url}"); + return null; + } + + // Check the status code of the response + if (response.statusCode != 200) { + showStatusCodeError(response.statusCode); + return null; + } + + // Convert the body of the response to a JSON object + String body = await response.transform(utf8.decoder).join(); + + try { + var data = json.decode(body); + + return data; + + } on FormatException { + + print("JSON format exception!"); + print("${body}"); + + showServerError( + "Format Exception", + "JSON data format exception:\n${body}" + ); + return null; + } } Map defaultHeaders() { @@ -496,6 +549,7 @@ class InvenTreeAPI { String _authorizationHeader(String username, String password) { if (_token.isNotEmpty) { + print("Using TOKEN: ${_token}"); return "Token $_token"; } else { return "Basic " + base64Encode(utf8.encode('${username}:${password}')); diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index da1f3add..16d0451e 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -166,39 +166,13 @@ class InvenTreeModel { */ Future reload(BuildContext context) async { - var response = await api.get(url, params: defaultGetFilters()) - .timeout(Duration(seconds: 10)) - .catchError((e) { - - if (e is SocketException) { - showServerError( - I18N.of(context).connectionRefused, - e.toString() - ); - } - else if (e is TimeoutException) { - showTimeoutError(context); - } else { - // Re-throw the error - throw e; - } - - return null; - }); + var response = await api.get(url, params: defaultGetFilters()); if (response == null) { return false; } - if (response.statusCode != 200) { - showStatusCodeError(response.statusCode); - print("Error retrieving data"); - return false; - } - - final Map data = json.decode(response.body); - - jsondata = data; + jsondata = response; return true; } @@ -265,34 +239,13 @@ class InvenTreeModel { print("GET: $addr ${params.toString()}"); - var response = await api.get(addr, params: params) - .timeout(Duration(seconds: 10)) - .catchError((e) { - - if (e is SocketException) { - showServerError(I18N.of(context).connectionRefused, e.toString()); - } - else if (e is TimeoutException) { - showTimeoutError(context); - } else { - // Re-throw the error - throw e; - } - return null; - }); + var response = await api.get(addr, params: params); if (response == null) { return null; } - if (response.statusCode != 200) { - showStatusCodeError(response.statusCode); - return null; - } - - final data = json.decode(response.body); - - return createFromJson(data); + return createFromJson(response); } Future create(BuildContext context, Map data) async { @@ -353,41 +306,21 @@ class InvenTreeModel { params["limit"] = "${limit}"; params["offset"] = "${offset}"; - var response = await api.get(URL, params: params) - .timeout(Duration(seconds: 10)) - .catchError((error) { - if (error is SocketException) { - showServerError( - I18N - .of(OneContext().context) - .connectionRefused, - error.toString() - ); - } - - return null; - }); + var response = await api.get(URL, params: params); if (response == null) { return null; } - if (response.statusCode != 200) { - showStatusCodeError(response.statusCode); - return null; - } - // Construct the response InvenTreePageResponse page = new InvenTreePageResponse(); - final data = json.decode(response.body); - - if (data.containsKey("count") && data.containsKey("results")) { - page.count = data["count"] as int; + if (response.containsKey("count") && response.containsKey("results")) { + page.count = response["count"] as int; page.results = []; - for (var result in data["results"]) { + for (var result in response["results"]) { page.addResult(createFromJson(result)); } @@ -417,50 +350,20 @@ class InvenTreeModel { print("LIST: $URL ${params.toString()}"); - // TODO - Add "timeout" - // TODO - Add error catching - - var response = await api.get(URL, params:params) - .timeout(Duration(seconds: 10)) - .catchError((e) { - - if (e is SocketException) { - showServerError( - I18N.of(context).connectionRefused, - e.toString() - ); - } - else if (e is TimeoutException) { - showTimeoutError(context); - } else { - // Re-throw the error - throw e; - } - - return null; - }); - - if (response == null) { - return null; - } + var response = await api.get(URL, params: params); // A list of "InvenTreeModel" items List results = new List(); - if (response.statusCode != 200) { - showStatusCodeError(response.statusCode); - - // Return empty list + if (response == null) { return results; } - final data = json.decode(response.body); - // TODO - handle possible error cases: // - No data receieved // - Data is not a list of maps - for (var d in data) { + for (var d in response) { // Create a new object (of the current class type InvenTreeModel obj = createFromJson(d);