Merge branch 'master' into master

This commit is contained in:
Maxime BELUGUET 2021-12-07 14:12:29 +01:00 committed by GitHub
commit dea78e02a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 231 additions and 137 deletions

View File

@ -44,6 +44,10 @@ This will generate a `crow_all.h` file which you can use in the following steps
## Building Crow's tests/examples
!!!note
This tutorial can be used for Crow projects built with CMake as well
1. Download and install [Homebrew](https://brew.sh).
2. Run `brew install cmake boost` in your terminal.
3. Get Crow's source code (the entire source code).
@ -55,3 +59,17 @@ This will generate a `crow_all.h` file which you can use in the following steps
!!!note
You can add options like `-DCROW_ENABLE_SSL`, `-DCROW_ENABLE_COMPRESSION`, or `-DCROW_AMALGAMATE` to `3.c` to build their tests/examples.
## Compiling using a compiler directly
All you need to do is run the following command:
```
g++ main.cpp -lpthread
```
!!!note
You'll need to install GCC via `brew install gcc`. the Clang compiler should be part of Xcode or Xcode command line tools.
You can use arguments like `-DCROW_ENABLE_DEBUG`, `-DCROW_ENABLE_COMPRESSION -lz` for HTTP Compression, or `-DCROW_ENABLE_SSL -lssl` for HTTPS support, or even replace g++ with clang++.
!!!warning
If you're using a version of boost prior to 1.69, you'll need to add the argument `-lboost_system` in order for you Crow application to compile correctly.

View File

@ -5,7 +5,7 @@ Here's how you can install Crow on your Windows machine.
Crow can be simply installed through VCPKG using the command `vcpkg install crow`
### Manually (source or release)
#### Microsoft Visual Studio 2019 and VCPKG
#### Microsoft Visual Studio and VCPKG
The following guide will use `example_with_all.cpp` as the Crow application for demonstration purposes. VCPKG will be used only to install Crow's dependencies.
1. Generate `crow_all.h` by navigating to the `scripts` folder and running `python3 merge_all.py ..\include crow_all.h`.

71
docs/guides/auth.md Normal file
View File

@ -0,0 +1,71 @@
While Crow doesn't directly support HTTP authentication, it does provide all the tools you need to build your own. This tutorial will show you how to setup basic and token authentication using Crow.
## Shared information
Every way boils down to the same basic flow:
- The handler calls a verification function.
- The handler provides a `request` and \<optionally\> a `response`.
- The function returns a `bool` or `enum` status.
- Handler either continues or stops executing based on the returned status.
- Either the function or handler modify and `end()` the `response` in case of failure.
For the purposes of this tutorial, we will assume that the verification function is defined as `#!cpp bool verify(crow::request req, crow::response res)`
## Basic Auth
Basic HTTP authentication requires the client to send the Username and Password as a single string, separated by a colon (':') and then encoded as base64. This data needs to be placed in the `Authorization` header of the request. A sample header using the credentials "Username" and "Password" would look like this: `Authorization: Basic VXNlcm5hbWU6UGFzc3dvcmQ=`.<br><br>
We don't need to worry about creating the request, we only need to extract the credentials from the `Authorization` header and verify them.
!!! note
There are multiple ways to verify the credentials. Most involve checking the username in a database, then checking a hash of the password against the stored password hash for that username. This tutorial will not go over them
<br>
To do this we first need to get the `Authorization` header as a string by using the following code:
```cpp
std::string myauth = req.get_header_value("Authorization");
```
<br>
Next we need to isolate our encoded credentials and decode them as follows:
```cpp
std::string mycreds = myauth.substr(6);
std::string d_mycreds = crow::utility::base64decode(mycreds, mycreds.size()/*, URLSafe? (true/false)*/);
```
<br>
Now that we have our `username:password` string, we only need to separate it into 2 different strings and verify their validity:
```cpp
size_t found = d_mycreds.find(':');
std::string username = d_mycreds.substr(0, found);
std::string password = d_mycreds.substr(found+1);
/*Verify validity of username and password here*/
return true; //or false if the username/password are invalid
```
## Token Auth
Tokens are some form of unique data that a server can provide to a client in order to verify the client's identity later. While on the surface level they don't provide more security than a strong password, they are designed to be less valuable by being *temporary* and providing *limited access*. Variables like expiration time and access scopes are heavily reliant on the implementation however.<br><br>
### Access Tokens
The kind of the token itself can vary depending on the implementation and project requirements: Many services use randomly generated strings as tokens. Then compare them against a database to retrieve the associated user data. Some services however prefer using data bearing tokens. One example of the latter kind is JWT, which uses JSON strings encoded in base64 and signed using a private key or an agreed upon secret. While this has the added hassle of signing the token to ensure that it's not been tampered with. It does allow for the client to issue tokens without ever needing to present a password or contact a server. The server would simply be able to verify the signature using the client's public key or secret.<br><br>
### Using an Access Token
Authenticating with an access token usually involves 2 stages: The first being acquiring the access token from an authority (either by providing credentials such as a username and a password to a server or generating a signed token). The scope of the token (what kind of information it can read or change) is usually defined in this step.<br><br>
The second stage is simply presenting the Token to the server when requesting a resource. This is even simpler than using basic authentication. All the client needs to do is provide the `Authorization` header with a keyword (usually `Bearer`) followed by the token itself (for example: `Authorization: Bearer ABC123`). Once the client has done that the server will need to acquire this token, which can easily be done as follows:<br>
```cpp
std::string myauth = req.get_header_value("Authorization");
std::string mycreds = myauth.substr(7); // The length can change based on the keyword used
/*Verify validity of the token here*/
return true; //or false if the token is invalid
```
<br>
The way of verifying the token is largely up to the implementation, and involves either Bearer token decoding and verification, or database access, neither of which is in this tutorial's scope.<br><br>
### Refresh Tokens
Some services may choose to provide a refresh token alongside the access token. this token can be used to request a new access token if the existing one has expired. It provides convenience and security in that it makes it possible to acquire new access tokens without the need to expose a password. The downside however is that it can allow a malicious entity to keep its access to a compromised account. As such refresh tokens need to be handled with care, kept secure, and always invalidated as soon as a client logs out or requests a new access token.
## Sessions
While Crow does not provide built in support for user sessions, a community member was kind enough to provide their own implementation on one of the related issue, their comment along with the code is available [here](https://github.com/CrowCpp/Crow/issues/144#issuecomment-860384771) (Please keep in mind that while we appreciate all efforts to push Crow forward, we cannot provide support for this implementation unless it becomes part of the core project).

View File

@ -39,6 +39,7 @@ namespace crow
using ssl_context_t = boost::asio::ssl::context;
#endif
/// The main server application
///
/// Use `SimpleApp` or `App<Middleware1, Middleware2, etc...>`
template<typename... Middlewares>
@ -57,6 +58,7 @@ namespace crow
{}
/// Process an Upgrade request
///
/// Currently used to upgrrade an HTTP connection to a WebSocket connection
template<typename Adaptor>
@ -79,10 +81,14 @@ namespace crow
///Create a route using a rule (**Use CROW_ROUTE instead**)
template<uint64_t Tag>
auto route(std::string&& rule)
#ifdef CROW_CAN_USE_CPP17
-> typename std::invoke_result<decltype(&Router::new_rule_tagged<Tag>), Router, std::string&&>::type
#ifdef CROW_GCC83_WORKAROUND
auto& route(std::string&& rule)
#else
auto route(std::string&& rule)
#endif
#if defined CROW_CAN_USE_CPP17 && !defined CROW_GCC83_WORKAROUND
-> typename std::invoke_result<decltype(&Router::new_rule_tagged<Tag>), Router, std::string&&>::type
#elif !defined CROW_GCC83_WORKAROUND
-> typename std::result_of<decltype (&Router::new_rule_tagged<Tag>)(Router, std::string&&)>::type
#endif
{
@ -157,6 +163,7 @@ namespace crow
}
/// Set the server's log level
///
/// Possible values are:<br>
/// crow::LogLevel::Debug (0)<br>
@ -218,6 +225,7 @@ namespace crow
}
#endif
/// A wrapper for `validate()` in the router
///
/// Go through the rules, upgrade them if possible, and add them to the list of rules
void validate()

View File

@ -526,7 +526,18 @@ namespace crow
{
is_writing = true;
boost::asio::write(adaptor_.socket(), buffers_);
res.do_stream_file(adaptor_);
if (res.file_info.statResult == 0)
{
std::ifstream is(res.file_info.path.c_str(), std::ios::in | std::ios::binary);
char buf[16384];
while (is.read(buf, sizeof(buf)).gcount() > 0)
{
std::vector<asio::const_buffer> buffers;
buffers.push_back(boost::asio::buffer(buf));
do_write_sync(buffers);
}
}
res.end();
res.clear();
@ -552,8 +563,30 @@ namespace crow
else
{
is_writing = true;
boost::asio::write(adaptor_.socket(), buffers_);
res.do_stream_body(adaptor_);
boost::asio::write(adaptor_.socket(), buffers_); // Write the response start / headers
if (res.body.length() > 0)
{
std::string buf;
std::vector<asio::const_buffer> buffers;
while (res.body.length() > 16384)
{
//buf.reserve(16385);
buf = res.body.substr(0, 16384);
res.body = res.body.substr(16384);
buffers.clear();
buffers.push_back(boost::asio::buffer(buf));
do_write_sync(buffers);
}
// Collect whatever is left (less than 16KB) and send it down the socket
// buf.reserve(is.length());
buf = res.body;
res.body.clear();
buffers.clear();
buffers.push_back(boost::asio::buffer(buf));
do_write_sync(buffers);
}
res.end();
res.clear();
@ -637,6 +670,31 @@ namespace crow
});
}
inline void do_write_sync(std::vector<asio::const_buffer>& buffers)
{
boost::asio::write(adaptor_.socket(), buffers, [&](std::error_code ec, std::size_t) {
if (!ec)
{
if (close_connection_)
{
adaptor_.shutdown_write();
adaptor_.close();
CROW_LOG_DEBUG << this << " from write (sync)(1)";
check_destroy();
}
return false;
}
else
{
CROW_LOG_ERROR << ec << " - happened while sending buffers";
CROW_LOG_DEBUG << this << " from write (sync)(2)";
check_destroy();
return true;
}
});
}
void check_destroy()
{
CROW_LOG_DEBUG << this << " is_reading " << is_reading << " is_writing " << is_writing;

View File

@ -53,7 +53,6 @@ namespace crow
return crow::get_header_value(headers, key);
}
// TODO find a better way to format this so that stuff aren't moved down a line
// clang-format off
response() {}
explicit response(int code) : code(code) {}
@ -122,6 +121,7 @@ namespace crow
}
/// Return a "Temporary Redirect" response.
///
/// Location can either be a route or a full URL.
void redirect(const std::string& location)
@ -131,6 +131,7 @@ namespace crow
}
/// Return a "Permanent Redirect" response.
///
/// Location can either be a route or a full URL.
void redirect_perm(const std::string& location)
@ -140,6 +141,7 @@ namespace crow
}
/// Return a "Found (Moved Temporarily)" response.
///
/// Location can either be a route or a full URL.
void moved(const std::string& location)
@ -149,6 +151,7 @@ namespace crow
}
/// Return a "Moved Permanently" response.
///
/// Location can either be a route or a full URL.
void moved_perm(const std::string& location)
@ -201,6 +204,7 @@ namespace crow
}
/// This constains metadata (coming from the `stat` command) related to any static files associated with this response.
///
/// Either a static file or a string body can be returned as 1 response.
struct static_file_info
@ -242,89 +246,10 @@ namespace crow
}
}
/// Stream a static file.
template<typename Adaptor>
void do_stream_file(Adaptor& adaptor)
{
if (file_info.statResult == 0)
{
std::ifstream is(file_info.path.c_str(), std::ios::in | std::ios::binary);
write_streamed(is, adaptor);
}
}
/// Stream the response body (send the body in chunks).
template<typename Adaptor>
void do_stream_body(Adaptor& adaptor)
{
if (body.length() > 0)
{
write_streamed_string(body, adaptor);
}
}
private:
bool completed_{};
std::function<void()> complete_request_handler_;
std::function<bool()> is_alive_helper_;
static_file_info file_info;
template<typename Stream, typename Adaptor>
void write_streamed(Stream& is, Adaptor& adaptor)
{
char buf[16384];
while (is.read(buf, sizeof(buf)).gcount() > 0)
{
std::vector<asio::const_buffer> buffers;
buffers.push_back(boost::asio::buffer(buf));
write_buffer_list(buffers, adaptor);
}
}
// THIS METHOD DOES MODIFY THE BODY, AS IN IT EMPTIES IT
template<typename Adaptor>
void write_streamed_string(std::string& is, Adaptor& adaptor)
{
std::string buf;
std::vector<asio::const_buffer> buffers;
while (is.length() > 16384)
{
//buf.reserve(16385);
buf = is.substr(0, 16384);
is = is.substr(16384);
push_and_write(buffers, buf, adaptor);
}
// Collect whatever is left (less than 16KB) and send it down the socket
// buf.reserve(is.length());
buf = is;
is.clear();
push_and_write(buffers, buf, adaptor);
}
template<typename Adaptor>
inline void push_and_write(std::vector<asio::const_buffer>& buffers, std::string& buf, Adaptor& adaptor)
{
buffers.clear();
buffers.push_back(boost::asio::buffer(buf));
write_buffer_list(buffers, adaptor);
}
template<typename Adaptor>
inline void write_buffer_list(std::vector<asio::const_buffer>& buffers, Adaptor& adaptor)
{
boost::asio::write(adaptor.socket(), buffers, [this](std::error_code ec, std::size_t) {
if (!ec)
{
return false;
}
else
{
CROW_LOG_ERROR << ec << " - happened while sending buffers";
this->end();
return true;
}
});
}
};
} // namespace crow

View File

@ -684,7 +684,7 @@ namespace crow
lremain_--;
}
/// determines num_type from the string.
/// Determines num_type from the string.
void determine_num_type()
{
if (t_ != type::Number)
@ -1221,8 +1221,9 @@ namespace crow
/// JSON write value.
///
/// Value can mean any json value, including a JSON object.
/// Value can mean any json value, including a JSON object.<br>
/// Write means this class is used to primarily assemble JSON objects using keys and values and export those into a string.
class wvalue : public returnable
{

View File

@ -1,4 +1,4 @@
// This file is generated from nginx/conf/mime.types using nginx_mime2cpp.py
// This file is generated from nginx/conf/mime.types using nginx_mime2cpp.py on 2021-12-03.
#include <unordered_map>
#include <string>
@ -21,6 +21,7 @@ namespace crow
{"jad", "text/vnd.sun.j2me.app-descriptor"},
{"wml", "text/vnd.wap.wml"},
{"htc", "text/x-component"},
{"avif", "image/avif"},
{"png", "image/png"},
{"svgz", "image/svg+xml"},
{"svg", "image/svg+xml"},
@ -58,6 +59,7 @@ namespace crow
{"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{"wmlc", "application/vnd.wap.wmlc"},
{"wasm", "application/wasm"},
{"7z", "application/x-7z-compressed"},
{"cco", "application/x-cocoa"},
{"jardiff", "application/x-java-archive-diff"},

View File

@ -23,6 +23,7 @@ namespace crow
};
///One part of the multipart message
///
/// It is usually separated from other sections by a `boundary`
struct part

View File

@ -11,6 +11,7 @@
namespace crow
{
/// A wrapper for `nodejs/http-parser`.
///
/// Used to generate a \ref crow.request from the TCP socket buffer.
template<typename Handler>

View File

@ -363,6 +363,7 @@ namespace crow
}
/// Get a value from a name, used for `?name=value`.
///
/// Note: this method returns the value of the first occurrence of the key only, to return all occurrences, see \ref get_list().
char* get (const std::string& name) const
@ -391,6 +392,7 @@ namespace crow
}
/// Returns a list of values, passed as `?name[]=value1&name[]=value2&...name[]=valuen` with n being the size of the list.
///
/// Note: Square brackets in the above example are controlled by `use_brackets` boolean (true by default). If set to false, the example becomes `?name=value1,name=value2...name=valuen`
std::vector<char*> get_list (const std::string& name, bool use_brackets = true) const
@ -429,6 +431,7 @@ namespace crow
}
/// Works similar to \ref get_list() except the brackets are mandatory must not be empty.
///
/// For example calling `get_dict(yourname)` on `?yourname[sub1]=42&yourname[sub2]=84` would give a map containing `{sub1 : 42, sub2 : 84}`.
///

View File

@ -22,6 +22,7 @@ namespace crow
constexpr const uint16_t INVALID_BP_ID{0xFFFF};
/// A base class for all rules.
///
/// Used to provide a common interface for code dealing with different types of rules.<br>
/// A Rule provides a URL, allowed HTTP methods, and handlers.
@ -362,6 +363,7 @@ namespace crow
/// A rule dealing with websockets.
///
/// Provides the interface for the user to put in the necessary handlers for a websocket to work.
class WebSocketRule : public BaseRule
@ -437,6 +439,7 @@ namespace crow
};
/// Allows the user to assign parameters using functions.
///
/// `rule.name("name").methods(HTTPMethod::POST)`
template<typename T>
@ -1084,6 +1087,7 @@ namespace crow
};
/// A blueprint can be considered a smaller section of a Crow app, specifically where the router is conecerned.
///
/// You can use blueprints to assign a common prefix to rules' prefix, set custom static and template folders, and set a custom catchall route.
/// You can also assign nest blueprints for maximum Compartmentalization.

View File

@ -57,3 +57,11 @@
#define noexcept throw()
#endif
#endif
#if defined(__GNUC__) && __GNUC__ == 8 && __GNUC_MINOR__ < 4
#if __cplusplus > 201103L
#define CROW_GCC83_WORKAROUND
#else
#error "GCC 8.1 - 8.3 has a bug that prevents crow from compiling with C++11. Please update GCC to > 8.3 or use C++ > 11."
#endif
#endif

View File

@ -41,15 +41,13 @@ namespace crow
CROW_LOG_DEBUG << "task_timer cancelled: " << this << ' ' << id;
}
///
/// Schedule the given task to be executed after the default amount of
/// ticks.
/// Schedule the given task to be executed after the default amount of ticks.
///
/// \return identifier_type Used to cancel the thread.
/// It is not bound to this task_timer instance and in some cases could lead to
/// undefined behavior if used with other task_timer objects or after the task
/// has been successfully executed.
///
identifier_type schedule(const task_type& task)
{
tasks_.insert(
@ -60,8 +58,8 @@ namespace crow
return highest_id_;
}
///
/// Schedule the given task to be executed after the given time.
///
/// \param timeout The amount of ticks (seconds) to wait before execution.
///
@ -69,7 +67,6 @@ namespace crow
/// It is not bound to this task_timer instance and in some cases could lead to
/// undefined behavior if used with other task_timer objects or after the task
/// has been successfully executed.
///
identifier_type schedule(const task_type& task, std::uint8_t timeout)
{
tasks_.insert({++highest_id_,
@ -78,16 +75,13 @@ namespace crow
return highest_id_;
}
///
/// Set the default timeout for this task_timer instance. (Default: 5)
///
/// \param timeout The amount of ticks (seconds) to wait before execution.
///
void set_default_timeout(std::uint8_t timeout) { default_timeout_ = timeout; }
///
/// \param timeout The amount of ticks (seconds) to wait before execution.
void set_default_timeout(std::uint8_t timeout) { default_timeout_ = timeout; }
/// Get the default timeout. (Default: 5)
///
std::uint8_t get_default_timeout() const { return default_timeout_; }
private:

View File

@ -64,12 +64,11 @@ namespace crow
class Connection : public connection
{
public:
///
/// Constructor for a connection.
///
/// Requires a request with an "Upgrade: websocket" header.<br>
/// Automatically handles the handshake.
///
Connection(const crow::request& req, Adaptor&& adaptor,
std::function<void(crow::websocket::connection&)> open_handler,
std::function<void(crow::websocket::connection&, const std::string&, bool)> message_handler,
@ -120,11 +119,10 @@ namespace crow
adaptor_.get_io_service().post(handler);
}
///
/// Send a "Ping" message.
///
/// Usually invoked to check if the other point is still online.
///
void send_ping(const std::string& msg) override
{
dispatch([this, msg] {
@ -135,11 +133,10 @@ namespace crow
});
}
///
/// Send a "Pong" message.
///
/// Usually automatically invoked as a response to a "Ping" message.
///
void send_pong(const std::string& msg) override
{
dispatch([this, msg] {
@ -172,11 +169,10 @@ namespace crow
});
}
///
/// Send a close signal.
///
/// Sets a flag to destroy the object once the message is sent.
///
void close(const std::string& msg) override
{
dispatch([this, msg] {
@ -224,11 +220,10 @@ namespace crow
}
}
///
/// Send the HTTP upgrade response.
///
/// Finishes the handshake process, then starts reading messages from the socket.
///
void start(std::string&& hello)
{
static std::string header = "HTTP/1.1 101 Switching Protocols\r\n"
@ -246,14 +241,13 @@ namespace crow
do_read();
}
///
/// Read a websocket message.
///
/// Involves:<br>
/// Handling headers (opcodes, size).<br>
/// Unmasking the payload.<br>
/// Reading the actual payload.<br>
///
void do_read()
{
is_reading = true;
@ -482,11 +476,10 @@ namespace crow
return (mini_header_ & 0x0f00) >> 8;
}
///
/// Process the payload fragment.
///
/// Unmasks the fragment, checks the opcode, merges fragments into 1 message body, and calls the appropriate handler.
///
void handle_fragment()
{
if (has_mask_)
@ -569,11 +562,10 @@ namespace crow
fragment_.clear();
}
///
/// Send the buffers' data through the socket.
///
/// Also destroyes the object if the Close flag is set.
///
void do_write()
{
if (sending_buffers_.empty())

View File

@ -53,6 +53,8 @@ nav:
- Compression: guides/compression.md
- Websockets: guides/websockets.md
- Writing Tests: guides/testing.md
- Using Crow:
- HTTP Authorization: guides/auth.md
- Server setup:
- Proxies: guides/proxies.md
- Systemd run on startup: guides/syste.md

View File

@ -3,23 +3,32 @@
#get mime.types file from the nginx repository at nginx/conf/mime.types
#typical output filename: mime_types.h
import sys
from datetime import date
if len(sys.argv) != 3:
print("Usage: {} <NGINX_MIME_TYPE_FILE_PATH> <CROW_OUTPUT_HEADER_PATH>".format(sys.argv[0]))
if (len(sys.argv) != 3) and (len(sys.argv) != 2):
print("Usage (local file): {} <NGINX_MIME_TYPE_FILE_PATH> <CROW_OUTPUT_HEADER_PATH>".format(sys.argv[0]))
print("(downloads file) : {} <CROW_OUTPUT_HEADER_PATH>".format(sys.argv[0]))
sys.exit(1)
if len(sys.argv) == 3:
file_path = sys.argv[1]
output_path = sys.argv[2]
elif len(sys.argv) == 2:
import requests
open("mime.types", "wb").write(requests.get("https://hg.nginx.org/nginx/raw-file/tip/conf/mime.types").content)
file_path = "mime.types"
output_path = sys.argv[1]
tabspace = " "
tabandahalfspace = " "
def main():
outLines = []
outLines.append("//This file is generated from nginx/conf/mime.types using nginx_mime2cpp.py")
outLines.append("// This file is generated from nginx/conf/mime.types using nginx_mime2cpp.py on " + date.today().strftime('%Y-%m-%d') + ".")
outLines.extend([
"#include <unordered_map>",
"#include <string>",
"",
"namespace crow {",
"namespace crow",
"{",
tabspace + "const std::unordered_map<std::string, std::string> mime_types{"])
with open(file_path, "r") as mtfile:
@ -39,11 +48,8 @@ def main():
incompleteExists = False
outLines.extend(mime_line_to_cpp(splitLine))
outLines[-1] = outLines[-1][:-1]
outLines.extend([
tabspace + "};",
"}"
])
outLines[-1] = outLines[-1][:-1] + "};"
outLines.append("}")
with open(output_path, "w") as mtcppfile:
mtcppfile.writelines(x + '\n' for x in outLines)
@ -55,7 +61,7 @@ def mime_line_to_cpp(mlist):
if (mlist[i].endswith(";")):
mlist[i] = mlist[i][:-1]
for i in range (len(mlist)-1, 0, -1):
stringReturn.append(tabspace*2 + "{\"" + mlist[i] + "\", \"" + mlist[0] + "\"},")
stringReturn.append(tabandahalfspace + "{\"" + mlist[i] + "\", \"" + mlist[0] + "\"},")
#print("created: " + stringReturn)
return stringReturn