Mercurial > hg > orthanc
diff OrthancFramework/Sources/Lua/LuaContext.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | Core/Lua/LuaContext.cpp@e3b3af80732d |
children | bf7b9edf6b81 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/Lua/LuaContext.cpp Wed Jun 10 20:30:34 2020 +0200 @@ -0,0 +1,690 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeaders.h" +#include "LuaContext.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include <set> +#include <cassert> +#include <boost/lexical_cast.hpp> + +extern "C" +{ +#include <lualib.h> +#include <lauxlib.h> +} + +namespace Orthanc +{ + static bool OnlyContainsDigits(const std::string& s) + { + for (size_t i = 0; i < s.size(); i++) + { + if (!isdigit(s[i])) + { + return false; + } + } + + return true; + } + + LuaContext& LuaContext::GetLuaContext(lua_State *state) + { + const void* value = GetGlobalVariable(state, "_LuaContext"); + assert(value != NULL); + + return *const_cast<LuaContext*>(reinterpret_cast<const LuaContext*>(value)); + } + + int LuaContext::PrintToLog(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // http://medek.wordpress.com/2009/02/03/wrapping-lua-errors-and-print-function/ + int nArgs = lua_gettop(state); + lua_getglobal(state, "tostring"); + + // Make sure you start at 1 *NOT* 0 for arrays in Lua. + std::string result; + + for (int i = 1; i <= nArgs; i++) + { + const char *s; + lua_pushvalue(state, -1); + lua_pushvalue(state, i); + lua_call(state, 1, 1); + s = lua_tostring(state, -1); + + if (result.size() > 0) + result.append(", "); + + if (s == NULL) + result.append("<No conversion to string>"); + else + result.append(s); + + lua_pop(state, 1); + } + + LOG(WARNING) << "Lua says: " << result; + that.log_.append(result); + that.log_.append("\n"); + + return 0; + } + + + int LuaContext::ParseJson(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + int nArgs = lua_gettop(state); + if (nArgs != 1 || + !lua_isstring(state, 1)) // Password + { + lua_pushnil(state); + return 1; + } + + const char* str = lua_tostring(state, 1); + + Json::Value value; + Json::Reader reader; + if (reader.parse(str, str + strlen(str), value)) + { + that.PushJson(value); + } + else + { + lua_pushnil(state); + } + + return 1; + } + + + int LuaContext::DumpJson(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + int nArgs = lua_gettop(state); + if ((nArgs != 1 && nArgs != 2) || + (nArgs == 2 && !lua_isboolean(state, 2))) + { + lua_pushnil(state); + return 1; + } + + bool keepStrings = false; + if (nArgs == 2) + { + keepStrings = lua_toboolean(state, 2) ? true : false; + } + + Json::Value json; + that.GetJson(json, state, 1, keepStrings); + + Json::FastWriter writer; + std::string s = writer.write(json); + lua_pushlstring(state, s.c_str(), s.size()); + + return 1; + } + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::SetHttpCredentials(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs != 2 || + !lua_isstring(state, 1) || // Username + !lua_isstring(state, 2)) // Password + { + LOG(ERROR) << "Lua: Bad parameters to SetHttpCredentials()"; + } + else + { + // Configure the HTTP client + const char* username = lua_tostring(state, 1); + const char* password = lua_tostring(state, 2); + that.httpClient_.SetCredentials(username, password); + } + + return 0; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + bool LuaContext::AnswerHttpQuery(lua_State* state) + { + std::string str; + + try + { + httpClient_.Apply(str); + } + catch (OrthancException&) + { + return false; + } + + // Return the result of the HTTP request + lua_pushlstring(state, str.c_str(), str.size()); + + return true; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + void LuaContext::SetHttpHeaders(int top) + { + std::map<std::string, std::string> headers; + GetDictionaryArgument(headers, lua_, top, false /* keep key case as provided by Lua script */); + + httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request + + for (std::map<std::string, std::string>::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + httpClient_.AddHeader(it->first, it->second); + } + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpGet(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs < 1 || nArgs > 2 || // check args count + !lua_isstring(state, 1)) // URL is a string + { + LOG(ERROR) << "Lua: Bad parameters to HttpGet()"; + lua_pushnil(state); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(HttpMethod_Get); + that.httpClient_.SetUrl(url); + that.httpClient_.GetBody().clear(); + that.SetHttpHeaders(2); + + // Do the HTTP GET request + if (!that.AnswerHttpQuery(state)) + { + LOG(ERROR) << "Lua: Error in HttpGet() for URL " << url; + lua_pushnil(state); + } + + return 1; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpPostOrPut(lua_State *state, + HttpMethod method) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if ((nArgs < 1 || nArgs > 3) || // check arg count + !lua_isstring(state, 1) || // URL is a string + (nArgs >= 2 && (!lua_isstring(state, 2) && !lua_isnil(state, 2)))) // Body data is null or is a string + { + LOG(ERROR) << "Lua: Bad parameters to HttpPost() or HttpPut()"; + lua_pushnil(state); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(method); + that.httpClient_.SetUrl(url); + that.SetHttpHeaders(3); + + if (nArgs >= 2 && !lua_isnil(state, 2)) + { + size_t bodySize = 0; + const char* bodyData = lua_tolstring(state, 2, &bodySize); + + if (bodySize == 0) + { + that.httpClient_.GetBody().clear(); + } + else + { + that.httpClient_.GetBody().assign(bodyData, bodySize); + } + } + else + { + that.httpClient_.GetBody().clear(); + } + + // Do the HTTP POST/PUT request + if (!that.AnswerHttpQuery(state)) + { + LOG(ERROR) << "Lua: Error in HttpPost() or HttpPut() for URL " << url; + lua_pushnil(state); + } + + return 1; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpPost(lua_State *state) + { + return CallHttpPostOrPut(state, HttpMethod_Post); + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpPut(lua_State *state) + { + return CallHttpPostOrPut(state, HttpMethod_Put); + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpDelete(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs < 1 || nArgs > 2 || !lua_isstring(state, 1)) // URL + { + LOG(ERROR) << "Lua: Bad parameters to HttpDelete()"; + lua_pushnil(state); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(HttpMethod_Delete); + that.httpClient_.SetUrl(url); + that.httpClient_.GetBody().clear(); + that.SetHttpHeaders(2); + + // Do the HTTP DELETE request + std::string s; + if (!that.httpClient_.Apply(s)) + { + LOG(ERROR) << "Lua: Error in HttpDelete() for URL " << url; + lua_pushnil(state); + } + else + { + lua_pushstring(state, "SUCCESS"); + } + + return 1; + } +#endif + + + void LuaContext::PushJson(const Json::Value& value) + { + if (value.isString()) + { + const std::string s = value.asString(); + lua_pushlstring(lua_, s.c_str(), s.size()); + } + else if (value.isDouble()) + { + lua_pushnumber(lua_, value.asDouble()); + } + else if (value.isInt()) + { + lua_pushinteger(lua_, value.asInt()); + } + else if (value.isUInt()) + { + lua_pushinteger(lua_, value.asUInt()); + } + else if (value.isBool()) + { + lua_pushboolean(lua_, value.asBool()); + } + else if (value.isNull()) + { + lua_pushnil(lua_); + } + else if (value.isArray()) + { + lua_newtable(lua_); + + // http://lua-users.org/wiki/SimpleLuaApiExample + for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) + { + // Push the table index (note the "+1" because of Lua conventions) + lua_pushnumber(lua_, i + 1); + + // Push the value of the cell + PushJson(value[i]); + + // Stores the pair in the table + lua_rawset(lua_, -3); + } + } + else if (value.isObject()) + { + lua_newtable(lua_); + + Json::Value::Members members = value.getMemberNames(); + + for (Json::Value::Members::const_iterator + it = members.begin(); it != members.end(); ++it) + { + // Push the index of the cell + lua_pushlstring(lua_, it->c_str(), it->size()); + + // Push the value of the cell + PushJson(value[*it]); + + // Stores the pair in the table + lua_rawset(lua_, -3); + } + } + else + { + throw OrthancException(ErrorCode_JsonToLuaTable); + } + } + + + void LuaContext::GetJson(Json::Value& result, + lua_State* state, + int top, + bool keepStrings) + { + if (lua_istable(state, top)) + { + Json::Value tmp = Json::objectValue; + bool isArray = true; + size_t size = 0; + + // Code adapted from: http://stackoverflow.com/a/6142700/881731 + + // Push another reference to the table on top of the stack (so we know + // where it is, and this function can work for negative, positive and + // pseudo indices + lua_pushvalue(state, top); + // stack now contains: -1 => table + lua_pushnil(state); + // stack now contains: -1 => nil; -2 => table + while (lua_next(state, -2)) + { + // stack now contains: -1 => value; -2 => key; -3 => table + // copy the key so that lua_tostring does not modify the original + lua_pushvalue(state, -2); + // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table + std::string key(lua_tostring(state, -1)); + Json::Value v; + GetJson(v, state, -2, keepStrings); + + tmp[key] = v; + + size += 1; + try + { + if (!OnlyContainsDigits(key) || + boost::lexical_cast<size_t>(key) != size) + { + isArray = false; + } + } + catch (boost::bad_lexical_cast&) + { + isArray = false; + } + + // pop value + copy of key, leaving original key + lua_pop(state, 2); + // stack now contains: -1 => key; -2 => table + } + // stack now contains: -1 => table (when lua_next returns 0 it pops the key + // but does not push anything.) + // Pop table + lua_pop(state, 1); + + // Stack is now the same as it was on entry to this function + + if (isArray) + { + result = Json::arrayValue; + for (size_t i = 0; i < size; i++) + { + result.append(tmp[boost::lexical_cast<std::string>(i + 1)]); + } + } + else + { + result = tmp; + } + } + else if (lua_isnil(state, top)) + { + result = Json::nullValue; + } + else if (!keepStrings && + lua_isboolean(state, top)) + { + result = lua_toboolean(state, top) ? true : false; + } + else if (!keepStrings && + lua_isnumber(state, top)) + { + // Convert to "int" if truncation does not loose precision + double value = static_cast<double>(lua_tonumber(state, top)); + int truncated = static_cast<int>(value); + + if (std::abs(value - static_cast<double>(truncated)) <= + std::numeric_limits<double>::epsilon()) + { + result = truncated; + } + else + { + result = value; + } + } + else if (lua_isstring(state, top)) + { + // Caution: The "lua_isstring()" case must be the last, since + // Lua can convert most types to strings by default. + result = std::string(lua_tostring(state, top)); + } + else if (lua_isboolean(state, top)) + { + result = lua_toboolean(state, top) ? true : false; + } + else + { + LOG(WARNING) << "Unsupported Lua type when returning Json"; + result = Json::nullValue; + } + } + + + LuaContext::LuaContext() + { + lua_ = luaL_newstate(); + if (!lua_) + { + throw OrthancException(ErrorCode_CannotCreateLua); + } + + luaL_openlibs(lua_); + lua_register(lua_, "print", PrintToLog); + lua_register(lua_, "ParseJson", ParseJson); + lua_register(lua_, "DumpJson", DumpJson); + +#if ORTHANC_ENABLE_CURL == 1 + lua_register(lua_, "HttpGet", CallHttpGet); + lua_register(lua_, "HttpPost", CallHttpPost); + lua_register(lua_, "HttpPut", CallHttpPut); + lua_register(lua_, "HttpDelete", CallHttpDelete); + lua_register(lua_, "SetHttpCredentials", SetHttpCredentials); +#endif + + SetGlobalVariable("_LuaContext", this); + } + + + LuaContext::~LuaContext() + { + lua_close(lua_); + } + + + void LuaContext::ExecuteInternal(std::string* output, + const std::string& command) + { + log_.clear(); + int error = (luaL_loadbuffer(lua_, command.c_str(), command.size(), "line") || + lua_pcall(lua_, 0, 0, 0)); + + if (error) + { + assert(lua_gettop(lua_) >= 1); + + std::string description(lua_tostring(lua_, -1)); + lua_pop(lua_, 1); /* pop error message from the stack */ + throw OrthancException(ErrorCode_CannotExecuteLua, description); + } + + if (output != NULL) + { + *output = log_; + } + } + + + bool LuaContext::IsExistingFunction(const char* name) + { + lua_settop(lua_, 0); + lua_getglobal(lua_, name); + return lua_type(lua_, -1) == LUA_TFUNCTION; + } + + + void LuaContext::Execute(Json::Value& output, + const std::string& command) + { + std::string s; + ExecuteInternal(&s, command); + + Json::Reader reader; + if (!reader.parse(s, output)) + { + throw OrthancException(ErrorCode_BadJson); + } + } + + + void LuaContext::RegisterFunction(const char* name, + lua_CFunction func) + { + lua_register(lua_, name, func); + } + + + void LuaContext::SetGlobalVariable(const char* name, + void* value) + { + lua_pushlightuserdata(lua_, value); + lua_setglobal(lua_, name); + } + + + const void* LuaContext::GetGlobalVariable(lua_State* state, + const char* name) + { + lua_getglobal(state, name); + assert(lua_type(state, -1) == LUA_TLIGHTUSERDATA); + const void* value = lua_topointer(state, -1); + lua_pop(state, 1); + return value; + } + + + void LuaContext::GetDictionaryArgument(std::map<std::string, std::string>& target, + lua_State* state, + int top, + bool keyToLowerCase) + { + target.clear(); + + if (lua_gettop(state) >= top) + { + Json::Value headers; + GetJson(headers, state, top, true); + + Json::Value::Members members = headers.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + std::string key = members[i]; + + if (keyToLowerCase) + { + Toolbox::ToLowerCase(key); + } + + target[key] = headers[members[i]].asString(); + } + } + } +}