diff --git a/.gitignore b/.gitignore index 4a62ba7..3e127e5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,8 @@ *.la *.a *.a.* +/build +/.vscode +.atom-build.cson +.atom-dbg.cson +/bin \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..49caa01 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,99 @@ +################ +# Config +################ + +# C++ project +language: cpp + +dist: trusty +sudo: required +group: edge + + +################ +# Services +################ + +services: + - docker + +################ +# Build matrix +################ + +matrix: + include: + + ################ + # Linux / GCC + ################ + + - os: linux + compiler: gcc + env: + - COMPILER_PACKAGE=g++-5 + - C_COMPILER=gcc-5 + - CXX_COMPILER=g++-5 + - CXXFLAGS=-std=c++11 + + - os: linux + env: + - COMPILER_PACKAGE=g++-6 + - C_COMPILER=gcc-6 + - CXX_COMPILER=g++-6 + + - os: linux + compiler: gcc + env: + - COMPILER_PACKAGE=g++-7 + - C_COMPILER=gcc-7 + - CXX_COMPILER=g++-7 + + ################ + # Linux / Clang + ################ + + - os: linux + env: + - COMPILER_PACKAGE=clang-4.0 + - C_COMPILER=clang-4.0 + - CXX_COMPILER=clang++-4.0 + + - os: linux + env: + - COMPILER_PACKAGE=clang-5.0 + - C_COMPILER=clang-5.0 + - CXX_COMPILER=clang++-5.0 + +before_install: + + # Show OS/compiler version (this may not be the same OS as the Docker container) + - uname -a + + # Use an artful container - gives us access to latest compilers. + - docker run -d --name ubuntu-test-container -v $(pwd):/travis ubuntu:artful tail -f /dev/null + - docker ps + + +install: + + # Create our container + - docker exec -t ubuntu-test-container bash -c "apt-get update -y && + apt-get --no-install-recommends install -y software-properties-common cmake + ninja-build libboost-all-dev libev-dev libuv1-dev ninja-build $COMPILER_PACKAGE && + apt-get -y clean && rm -rf /var/lib/apt/lists/*" + +################ +# Build / Test +################ + +script: + + # Run the container that we created and build the code + - docker exec -t ubuntu-test-container bash -c "cd /travis && + export CC=/usr/bin/$C_COMPILER && + export CXX=/usr/bin/$CXX_COMPILER && + mkdir build.release && cd build.release && + cmake ${CMAKE_OPTIONS} -DAMQP-CPP_BUILD_EXAMPLES=ON -DAMQP-CPP_LINUX_TCP=ON -GNinja .. && + cmake --config Release --build . && + cd .." diff --git a/CMakeLists.txt b/CMakeLists.txt index a902dde..0b0f054 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,36 @@ -cmake_minimum_required(VERSION 2.8) +# Builds AMQP-CPP +# +# Options: +# +# - AMQP-CPP_BUILD_SHARED (default OFF) +# ON: Build shared lib +# OFF: Build static lib +# +# - AMQP-CPP_LINUX_TCP (default OFF) +# ON: Build posix handler implementation +# OFF: Don't build posix handler implementation -project(amqp-cpp) +cmake_minimum_required(VERSION 3.2 FATAL_ERROR) + +# project name +project(amqpcpp) + +# build options +option(AMQP-CPP_BUILD_SHARED "Build shared library. If off, build will be static." OFF) +option(AMQP-CPP_LINUX_TCP "Build linux sockets implementation." OFF) +option(AMQP-CPP_BUILD_EXAMPLES "Build amqpcpp examples" OFF) # ensure c++11 on all compilers -include(set_cxx_norm.cmake) -set_cxx_norm (${CXX_NORM_CXX11}) +set (CMAKE_CXX_STANDARD 11) +# add source files +# ------------------------------------------------------------------------------------------------------ + +# set include/ as include directory +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) + +# macro that adds a list of provided source files to a list called SRCS. +# if variable SRCS does not yet exist, it is created. macro (add_sources) file (RELATIVE_PATH _relPath "${PROJECT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}") foreach (_src ${ARGN}) @@ -21,36 +46,62 @@ macro (add_sources) endif() endmacro() +# add source files add_subdirectory(src) -add_subdirectory(include) - -option(BUILD_SHARED "build shared library" OFF) - -if(BUILD_SHARED) - add_library(amqp-cpp SHARED ${SRCS}) - set_target_properties(amqp-cpp PROPERTIES SOVERSION 2.7) - install(TARGETS amqp-cpp - LIBRARY DESTINATION lib - ) -else() - add_library(amqp-cpp STATIC ${SRCS}) - install(TARGETS amqp-cpp - ARCHIVE DESTINATION lib - ) -endif() -Include_directories(${PROJECT_SOURCE_DIR}) -install(DIRECTORY include/ DESTINATION include/amqpcpp - FILES_MATCHING PATTERN "*.h") -install(FILES amqpcpp.h DESTINATION include) - -option(BUILD_TUTORIALS "build rabbitmq tutorials" OFF) -if(BUILD_TUTORIALS) -# add_subdirectory(examples/rabbitmq_tutorials) +if(AMQP-CPP_LINUX_TCP) + add_subdirectory(src/linux_tcp) endif() -set(AMQP-CPP_INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) -set(AMQP-CPP_INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR} PARENT_SCOPE) +# potentially build the examples +if(AMQP-CPP_BUILD_EXAMPLES) + add_subdirectory(examples) +endif() +# settings for specific compilers +# ------------------------------------------------------------------------------------------------------ + +# we have to prevent windows from defining the max macro. if (WIN32) - add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN) + add_definitions(-DNOMINMAX) endif() + +# build targets +# ------------------------------------------------------------------------------------------------------ + +# set output directory +set(LIBRARY_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/bin) + +if(AMQP-CPP_BUILD_SHARED) + # create shared lib + add_library(${PROJECT_NAME} SHARED ${SRCS}) + # set shared lib version + set_target_properties(${PROJECT_NAME} PROPERTIES SOVERSION 3.0) +else() + # create static lib + add_library(${PROJECT_NAME} STATIC ${SRCS}) +endif() + +# install rules +# ------------------------------------------------------------------------------------------------------ + +if(AMQP-CPP_BUILD_SHARED) + # copy shared lib and its static counter part + install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Config + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION lib + ) +else() + # copy static lib + install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Config + ARCHIVE DESTINATION lib + ) +endif() + +# copy header files +install(DIRECTORY include/amqpcpp/ DESTINATION include/amqpcpp + FILES_MATCHING PATTERN "*.h") +install(FILES include/amqpcpp.h DESTINATION include) + +install(EXPORT ${PROJECT_NAME}Config DESTINATION cmake) +export(TARGETS ${PROJECT_NAME} FILE ${PROJECT_NAME}Config.cmake) diff --git a/Makefile b/Makefile index d3575b0..a53f610 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ PREFIX ?= /usr INCLUDE_DIR = ${PREFIX}/include LIBRARY_DIR = ${PREFIX}/lib export LIBRARY_NAME = amqpcpp -export SONAME = 2.7 -export VERSION = 2.7.1 +export SONAME = 3.0 +export VERSION = 3.0.1 all: $(MAKE) -C src all @@ -25,9 +25,11 @@ clean: install: mkdir -p ${INCLUDE_DIR}/$(LIBRARY_NAME) + mkdir -p ${INCLUDE_DIR}/$(LIBRARY_NAME)/linux_tcp mkdir -p ${LIBRARY_DIR} - cp -f $(LIBRARY_NAME).h ${INCLUDE_DIR} - cp -f include/*.h ${INCLUDE_DIR}/$(LIBRARY_NAME) + cp -f include/$(LIBRARY_NAME).h ${INCLUDE_DIR} + cp -f include/amqpcpp/*.h ${INCLUDE_DIR}/$(LIBRARY_NAME) + cp -f include/amqpcpp/linux_tcp/*.h ${INCLUDE_DIR}/$(LIBRARY_NAME)/linux_tcp -cp -f src/lib$(LIBRARY_NAME).so.$(VERSION) ${LIBRARY_DIR} -cp -f src/lib$(LIBRARY_NAME).a.$(VERSION) ${LIBRARY_DIR} ln -r -s -f $(LIBRARY_DIR)/lib$(LIBRARY_NAME).so.$(VERSION) $(LIBRARY_DIR)/lib$(LIBRARY_NAME).so.$(SONAME) diff --git a/README.md b/README.md index 9b0165a..1ee1bd3 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,55 @@ AMQP-CPP ======== +[![Build Status](https://travis-ci.org/CopernicaMarketingSoftware/AMQP-CPP.svg?branch=master)](https://travis-ci.org/CopernicaMarketingSoftware/AMQP-CPP) + AMQP-CPP is a C++ library for communicating with a RabbitMQ message broker. The library can be used to parse incoming data from a RabbitMQ server, and to generate frames that can be sent to a RabbitMQ server. -This library has a layered architecture, and allows you - if you like - to -completely take care of the network layer. If you want to set up and manage the +This library has a layered architecture, and allows you - if you like - to +completely take care of the network layer. If you want to set up and manage the network connections yourself, the AMQP-CPP library will not make a connection to -RabbitMQ by itself, nor will it create sockets and/or perform IO operations. As -a user of this library, you create the socket connection and implement a certain -interface that you pass to the AMQP-CPP library and that the library will use +RabbitMQ by itself, nor will it create sockets and/or perform IO operations. As +a user of this library, you create the socket connection and implement a certain +interface that you pass to the AMQP-CPP library and that the library will use for IO operations. -Intercepting this network layer is however optional, the AMQP-CPP library also -comes with a predefined Tcp module that can be used if you trust the AMQP library +Intercepting this network layer is however optional, the AMQP-CPP library also +comes with a predefined Tcp module that can be used if you trust the AMQP library to take care of the network handling. In that case, the AMQP-CPP library does all the system calls to set up network connections and send and receive the data. -This layered architecture makes the library extremely flexible and portable: it -does not necessarily rely on operating system specific IO calls, and can be +This layered architecture makes the library extremely flexible and portable: it +does not necessarily rely on operating system specific IO calls, and can be easily integrated into any kind of event loop. If you want to implement the AMQP -protocol on top of some [unusual other communication layer](https://tools.ietf.org/html/rfc1149), -this library can be used for that - but if you want to use it with regular TCP -connections, setting it up is just as easy. +protocol on top of some [unusual other communication layer](https://tools.ietf.org/html/rfc1149), +this library can be used for that - but if you want to use it with regular TCP +connections, setting it up is just as easy. -AMQP-CPP is fully asynchronous and does not do any blocking (system) calls, so +AMQP-CPP is fully asynchronous and does not do any blocking (system) calls, so it can be used in high performance applications without the need for threads. -The AMQP-CPP library uses C++11 features, so if you intend use it, please make +The AMQP-CPP library uses C++11 features, so if you intend use it, please make sure that your compiler is up-to-date and supports C++11. -**Note for the reader:** This readme file has a peculiar structure. We start +**Note for the reader:** This readme file has a peculiar structure. We start explaining the pure and hard core low level interface in which you have to take care of opening socket connections yourself. In reality, you probably want -to use the simpler TCP interface that is being described later on. +to use the simpler TCP interface that is being described [later on](#tcp-connections). ABOUT ===== This library is created and maintained by Copernica (www.copernica.com), and is -used inside the MailerQ (www.mailerq.com), Yothalot (www.yothalot.com) and -AMQPipe (www.amqpipe.com) applications. MailerQ is a tool for sending large +used inside the MailerQ (www.mailerq.com), Yothalot (www.yothalot.com) and +AMQPipe (www.amqpipe.com) applications. MailerQ is a tool for sending large volumes of email, using AMQP message queues, Yothalot is a big data processing -map/reduce framework and AMQPipe is a tool for high-speed processing messages +map/reduce framework and AMQPipe is a tool for high-speed processing messages between AMQP pipes -Do you appreciate our work and are you looking for high quality email solutions? +Do you appreciate our work and are you looking for high quality email solutions? Then check out our other commercial and open source solutions: * Copernica Marketing Suite (www.copernica.com) @@ -63,35 +65,68 @@ Then check out our other commercial and open source solutions: INSTALLING ========== -If you are on some kind of Linux environment, installing the library is as easy -as running `make` and `make install`. This will install the full version of -the AMQP-CPP, including the system specific TCP module. If you do not need the -additional TCP module (because you take care of handling the network stuff -yourself), you can also compile a pure form of the library. Use `make pure` -and `make install` for that. +AMQP-CPP comes with an optional Linux-only TCP module that takes care of the +network part required for the AMQP-CPP core library. If you use this module, you +are required to link with `pthread` and `dl`. -For users on a non-Linux environment: this library is known to work on your -environment too (after all, it does not do any operating specific system calls), -but it might take some extra effort to get your compiler to compile it. Please -send in your pull requests once you have it running, so that others can benefit -from your experiences. +There are two methods to compile AMQP-CPP: CMake and Make. CMake is platform portable +and works on all systems, while the Makefile only works on Linux. Building of a shared +library is currently not supported on Windows. + +After building there are two relevant files to include when using the library. + + File | Include when? +---------------------|-------------------------------------------------------- + amqpcpp.h | Always + amqpcpp/linux_tcp.h | If using the Linux-only TCP module + +On Windows you are required to define `NOMINMAX` when compiling code that includes public AMQP-CPP header files. + +## Using cmake + +The CMake file supports both building and installing. You can choose not to use +the install functionality, and instead manually use the build output at `build/bin/`. Keep +in mind that the TCP module is only supported for Linux. An example install method +would be: + +```bash +mkdir build +cd build +cmake .. [-DAMQP-CPP_AMQBUILD_SHARED] [-DAMQP-CPP_LINUX_TCP] +cmake --build .. --target install +``` + + Option | Default | Meaning +-------------------------|---------|----------------------------------------------------------------------- + AMQP-CPP_BUILD_SHARED | OFF | Static lib(ON) or shared lib(OFF)? Shared is not supported on Windows. + AMQP-CPP_LINUX_TCP | OFF | Should the Linux-only TCP module be built? + +## Using make + +Compiling and installing AMQP-CPP with make is as easy as running `make` and +then `make install`. This will install the full version of AMQP-CPP, including +the system specific TCP module. If you do not need the additional TCP module +(because you take care of handling the network stuff yourself), you can also +compile a pure form of the library. Use `make pure` and `make install` for that. When you compile an application that uses the AMQP-CPP library, do not forget to link with the library. For gcc and clang the linker flag is -lamqpcpp. -If you use the fullblown version of AMQP-CPP (with the TCP module), you also -need to pass a -lpthread linker flag, because the TCP module uses a thread -for running an asynchronous and non-blocking DNS hostname lookup. +If you use the fullblown version of AMQP-CPP (with the TCP module), you also +need to pass the -lpthread and -ldl linker flags, because the TCP module uses a +thread for running an asynchronous and non-blocking DNS hostname lookup, and it +may dynamically look up functions from the openssl library if a secure connection +to RabbitMQ has to be set up. HOW TO USE AMQP-CPP =================== As we mentioned above, the library can be used in a network-agnostic fashion. -It then does not do any IO by itself, and you need to pass an object to the +It then does not do any IO by itself, and you need to pass an object to the library that the library can use for IO. So, before you start using the -library, you first you need to create a class that extends from the -ConnectionHandler base class. This is a class with a number of methods that are -called by the library every time it wants to send out data, or when it needs to +library, you first you need to create a class that extends from the +ConnectionHandler base class. This is a class with a number of methods that are +called by the library every time it wants to send out data, or when it needs to inform you that an error occured. ````c++ @@ -204,16 +239,16 @@ example call the "send()" or "write()" system call to send out the data to the RabbitMQ server. But what about data in the other direction? How does the library receive data back from RabbitMQ? -In this raw setup, the AMQP-CPP library does not do any IO by itself and it is -therefore also not possible for the library to receive data from a -socket. It is again up to you to do this. If, for example, you notice in your -event loop that the socket that is connected with the RabbitMQ server becomes -readable, you should read out that socket (for example by using the recv() system -call), and pass the received bytes to the AMQP-CPP library. This is done by +In this raw setup, the AMQP-CPP library does not do any IO by itself and it is +therefore also not possible for the library to receive data from a +socket. It is again up to you to do this. If, for example, you notice in your +event loop that the socket that is connected with the RabbitMQ server becomes +readable, you should read out that socket (for example by using the recv() system +call), and pass the received bytes to the AMQP-CPP library. This is done by calling the parse() method in the Connection object. The Connection::parse() method gets two parameters, a pointer to a buffer of -data that you just read from the socket, and a parameter that holds the size of +data that you just read from the socket, and a parameter that holds the size of this buffer. The code snippet below comes from the Connection.h C++ header file. ````c++ @@ -249,9 +284,9 @@ both the old data, and the new data. To optimize your calls to the parse() method, you _could_ use the Connection::expected() and Connection::maxFrame() methods. The expected() method returns the number of bytes that the library prefers to receive next. It is pointless to call the parse() method -with a smaller buffer, and it is best to call the method with a buffer of exactly this -size. The maxFrame() returns the max frame size for AMQP messages. If you read your -messages into a reusable buffer, you could allocate this buffer up to this size, so that +with a smaller buffer, and it is best to call the method with a buffer of exactly this +size. The maxFrame() returns the max frame size for AMQP messages. If you read your +messages into a reusable buffer, you could allocate this buffer up to this size, so that you never will have to reallocate. @@ -259,21 +294,22 @@ TCP CONNECTIONS =============== Although the AMQP-CPP library gives you extreme flexibility by letting you setup -your own network connections, the reality is that most if not all AMQP connections -use the TCP protocol. To help you out, the library therefore also comes with a +your own network connections, the reality is that most if not all AMQP connections +use the TCP protocol. To help you out, the library therefore also comes with a TCP module that takes care of setting up the network connections, and sending -and receiving the data. +and receiving the data. If you want to use this TCP module, you should not use the AMQP::Connection and AMQP::Channel classes that you saw above, but the alternative AMQP::TcpConnection and AMQP::TcpChannel classes instead. You also do not have to create your own class -that implements the "AMQP::ConnectionHandler" interface - but a class that inherits -from "AMQP::TcpHandler" instead. You especially need to implement the "monitor()" -method in that class, as that is needed by the AMQP-CPP library to interact with +that implements the "AMQP::ConnectionHandler" interface - but a class that inherits +from "AMQP::TcpHandler" instead. You especially need to implement the "monitor()" +method in that class, as that is needed by the AMQP-CPP library to interact with the main event loop: ````c++ #include +#include class MyTcpHandler : public AMQP::TcpHandler { @@ -334,7 +370,7 @@ class MyTcpHandler : public AMQP::TcpHandler // descriptor to the main application event loop (like the select() or // poll() loop). When the event loop reports that the descriptor becomes // readable and/or writable, it is up to you to inform the AMQP-CPP - // library that the filedescriptor is active by calling the + // library that the filedescriptor is active by calling the // connection->process(fd, flags) method. } }; @@ -344,8 +380,8 @@ The "monitor()" method can be used to integrate the AMQP filedescriptors in your application's event loop. For some popular event loops (libev, libevent), we have already added example handler objects (see the next section for that). -Using the TCP module of the AMQP-CPP library is easier than using the -raw AMQP::Connection and AMQP::Channel objects, because you do not have to +Using the TCP module of the AMQP-CPP library is easier than using the +raw AMQP::Connection and AMQP::Channel objects, because you do not have to create the sockets and connections yourself, and you also do not have to take care of buffering network data. @@ -371,14 +407,97 @@ channel.declareQueue("my-queue"); channel.bindQueue("my-exchange", "my-queue", "my-routing-key"); ```` +SECURE CONNECTIONS +================== + +The TCP module of AMQP-CPP also supports setting up secure connections. If your +RabbitMQ server accepts SSL connections, you can specify the address to your +server using the amqps:// protocol: + +````c++ +// init the SSL library (this works for openssl 1.1, for openssl 1.0 use SSL_library_init()) +OPENSSL_init_ssl(0, NULL); + +// address of the server (secure!) +AMQP::Address address("amqps://guest:guest@localhost/vhost"); + +// create a AMQP connection object +AMQP::TcpConnection connection(&myHandler, address); +```` + +There are two things to take care of if you want to create a secure connection: +(1) you must link your application with the -lssl flag (or use dlopen()), and (2) +you must initialize the openssl library by calling OPENSSL_init_ssl(). This +initializating must take place before you let you application connect to RabbitMQ. +This is necessary because AMQP-CPP needs access to the openssl library to set up +secure connections. It can only access this library if you have linked your +application with this library, or if you have loaded this library at runtime +using dlopen()). + +Linking openssl is the normal thing to do. You just have to add the `-lssl` flag +to your linker. If you however do not want to link your application with openssl, +you can also load the openssl library at runtime, and pass in the pointer to the +handle to AMQP-CPP: + +````c++ +// dynamically open the openssl library +void *handle = dlopen("/path/to/openssl.so", RTLD_LAZY); + +// tell AMQP-CPP library where the handle to openssl can be found +AMQP::openssl(handle); + +// @todo call functions to initialize openssl, and create the AMQP connection +// (see exampe above) +```` + +By itself, AMQP-CPP does not check if the created TLS connection is sufficient +secure. Whether the certificate is expired, self-signed, missing or invalid: for +AMQP-CPP it all doesn't matter and the connection is simply permitted. If you +want to be more strict (for example: if you want to verify the server's certificate), +you must do this yourself by implementing the "onSecured()" method in your handler +object: + +````c++ +#include +#include + +class MyTcpHandler : public AMQP::TcpHandler +{ + /** + * Method that is called right after the TLS connection has been created. + * In this method you can check the connection properties (like the certificate) + * and return false if you find it not secure enough + * @param connection the connection that has just completed the tls handshake + * @param ssl SSL structure from the openssl library + * @return bool true if connection is secure enough to start the AMQP protocol + */ + virtual bool onSecure(AMQP::TcpConnection *connection, const SSL *ssl) override + { + // @todo call functions from the openssl library to check the certificate, + // like SSL_get_peer_certificate() or SSL_get_verify_result(). + // For now we always allow the connection to proceed + return true; + } + + /** + * All other methods (like onConnected(), onError(), etc) are left out of this + * example, but would be here if this was an actual user space handler class. + */ +}; +```` + +The SSL pointer that is passed to the onSecured() method refers to the "SSL" +structure from the openssl library. + + EXISTING EVENT LOOPS ==================== Both the pure AMQP::Connection as well as the easier AMQP::TcpConnection class allow you to integrate AMQP-CPP in your own event loop. Whether you take care -of running the event loop yourself (for example by using the select() system -call), or if you use an existing library for it (like libevent, libev or libuv), -you can implement the "monitor()" method to watch the file descriptors and +of running the event loop yourself (for example by using the select() system +call), or if you use an existing library for it (like libevent, libev or libuv), +you can implement the "monitor()" method to watch the file descriptors and hand over control back to AMQP-CPP when one of the sockets become active. For libev and libevent users, we have even implemented an example implementation, @@ -394,26 +513,26 @@ int main() { // access to the event loop auto *loop = EV_DEFAULT; - + // handler for libev (so we don't have to implement AMQP::TcpHandler!) AMQP::LibEvHandler handler(loop); - + // make a connection AMQP::TcpConnection connection(&handler, AMQP::Address("amqp://localhost/")); - + // we need a channel too AMQP::TcpChannel channel(&connection); - + // create a temporary queue channel.declareQueue(AMQP::exclusive).onSuccess([&connection](const std::string &name, uint32_t messagecount, uint32_t consumercount) { - + // report the name of the temporary queue std::cout << "declared queue " << name << std::endl; - + // now we can close the connection connection.close(); }); - + // run the loop ev_run(loop, 0); @@ -426,18 +545,84 @@ The AMQP::LibEvHandler and AMQP::LibEventHandler classes are extended AMQP::TcpH classes, with an implementation of the monitor() method that simply adds the filedescriptor to the event loop. If you use this class however, it is recommended not to instantiate it directly (like we did in the example), but to create your own -"MyHandler" class that extends from it, and in which you also implement the +"MyHandler" class that extends from it, and in which you also implement the onError() method to report possible connection errors to your end users. -Currently, we have only added such an example TcpHandler implementation for libev and -libevent. For other event loops (like libuv and boost asio) we do not yet have +Currently, we have example TcpHandler implementations for libev, +libevent, and Boost's asio. For other event loops (like libuv) we do not yet have such examples. +| TCP Handler Impl | Header File Location | Sample File Location | +| ----------------------- | ---------------------- | --------------------- | +| Boost asio (io_service) | include/libboostasio.h | tests/libboostasio.cpp | +| libev | include/libev.h | tests/libev.cpp | +| libevent | include/libevent.h | tests/libevent.cpp | +| libuv | include/libuv.h | (Not available) | + +HEARTBEATS +========== + +The AMQP protocol supports *heartbeats*. If this heartbeat feature is enabled, the +client and the server negotiate a heartbeat interval during connection setup, and +they agree to send at least *some kind of data* over the connection during every +iteration of that interval. The normal data that is sent over the connection (like +publishing or consuming messages) is normally sufficient to keep the connection alive, +but if the client or server was idle during the negotiated interval time, a dummy +heartbeat message must be sent instead. + +The default behavior of the AMQP-CPP library is to disable heartbeats. The +proposed heartbeat interval of the server during connection setup (the server +normally suggests an interval of 60 seconds) is vetoed by the AMQP-CPP library so +no heartbeats are ever needed to be sent over the connection. This means that you +can safely keep your AMQP connection idle for as long as you like, and/or run long +lasting algorithms after you've consumed a message from RabbitMQ, without having +to worry about the connection being idle for too long. + +You can however choose to enable these heartbeats. If you want to enable heartbeats, +simple implement the onNegotiate() method inside your ConnectionHandler or +TcpHandler class and have it return the interval that you find appropriate. + +````c++ +#include + +class MyTcpHandler : public AMQP::TcpHandler +{ + /** + * Method that is called when the server tries to negotiate a heartbeat + * interval, and that is overridden to get rid of the default implementation + * (which vetoes the suggested heartbeat interval), and accept the interval + * instead. + * @param connection The connection on which the error occured + * @param interval The suggested interval in seconds + */ + virtual void onNegotiate(AMQP::TcpConnection *connection, uint16_t interval) + { + // we accept the suggestion from the server, but if the interval is smaller + // that one minute, we will use a one minute interval instead + if (interval < 60) interval = 60; + + // @todo + // set a timer in your event loop, and make sure that you call + // connection->heartbeat() every _interval_ seconds. + + // return the interval that we want to use + return interval; + } +}; +```` + +If you have enabled heartbeats, it is your own responsibility to ensure that the +```connection->heartbeat()``` method is called at least once during this period, +or that you call one of the other channel or connection methods to send data +over the connection. + +In the libev event loop implementation the heartbeats are enabled by default. + CHANNELS ======== -In the above example we created a channel object. A channel is a sort of virtual +In the above example we created a channel object. A channel is a sort of virtual connection, and it is possible to create many channels that all use the same connection. @@ -451,8 +636,8 @@ documented. All operations that you can perform on a channel are non-blocking. This means that it is not possible for a method (like Channel::declareExchange()) to -immediately return 'true' or 'false'. Instead, almost every method of the Channel -class returns an instance of the 'Deferred' class. This 'Deferred' object can be +immediately return 'true' or 'false'. Instead, almost every method of the Channel +class returns an instance of the 'Deferred' class. This 'Deferred' object can be used to install handlers that will be called in case of success or failure. For example, if you call the channel.declareExchange() method, the AMQP-CPP library @@ -544,7 +729,7 @@ channel object right after it was constructed. CHANNEL ERRORS ============== -It is important to realize that any error that occurs on a channel, +It is important to realize that any error that occurs on a channel, invalidates the entire channel, including all subsequent instructions that were already sent over it. This means that if you call multiple methods in a row, and the first method fails, all subsequent methods will not be executed either: @@ -576,7 +761,7 @@ requires and extra instruction to be sent to the RabbitMQ server, so some extra bytes are sent over the network, and some additional resources in both the client application and the RabbitMQ server are used (although this is all very limited). -If possible, it is best to make use of this feature. For example, if you have an important AMQP +If possible, it is best to make use of this feature. For example, if you have an important AMQP connection that you use for consuming messages, and at the same time you want to send another instruction to RabbitMQ (like declaring a temporary queue), it is best to set up a new channel for this 'declare' instruction. If the declare fails, @@ -591,7 +776,7 @@ void myDeclareMethod(AMQP::Connection *connection) { // create temporary channel to declare a queue AMQP::Channel channel(connection); - + // declare the queue (the channel object is destructed before the // instruction reaches the server, but the AMQP-CPP library can deal // with this) @@ -710,44 +895,54 @@ in almost any form: ````c++ /** * Publish a message to an exchange - * - * The following flags can be used - * - * - mandatory if set, an unroutable message will be sent back to - * the client (currently not supported) - * - * - immediate if set, a message that could not immediately be consumed - * is returned to the client (currently not supported) - * - * If either of the two flags is set, and the message could not immediately - * be published, the message is returned by the server to the client. However, - * at this moment in time, the AMQP-CPP library does not support catching - * such returned messages. - * + * + * You have to supply the name of an exchange and a routing key. RabbitMQ will + * then try to send the message to one or more queues. With the optional flags + * parameter you can specify what should happen if the message could not be routed + * to a queue. By default, unroutable message are silently discarded. + * + * This method returns a reference to a DeferredPublisher object. You can use + * this returned object to install callbacks that are called when an undeliverable + * message is returned, or to set the callback that is called when the server + * confirms that the message was received. + * + * To enable handling returned messages, or to enable publisher-confirms, you must + * not only set the callback, but also pass in appropriate flags to enable this + * feature. If you do not pass in these flags, your callbacks will not be called. + * If you are not at all interested in returned messages or publish-confirms, you + * can ignore the flag and the returned object. + * + * Watch out: the channel returns _the same_ DeferredPublisher object for all + * calls to the publish() method. This means that the callbacks that you install + * for the first published message are also used for subsequent messages _and_ + * it means that if you install a different callback for a later publish + * operation, it overwrites your earlier callbacks + * + * The following flags can be supplied: + * + * - mandatory If set, server returns messages that are not sent to a queue + * - immediate If set, server returns messages that can not immediately be forwarded to a consumer. + * * @param exchange the exchange to publish to * @param routingkey the routing key - * @param flags optional flags (see above) * @param envelope the full envelope to send * @param message the message to send * @param size size of the message + * @param flags optional flags */ -bool publish(const std::string &exchange, const std::string &routingKey, int flags, const AMQP::Envelope &envelope); -bool publish(const std::string &exchange, const std::string &routingKey, const AMQP::Envelope &envelope); -bool publish(const std::string &exchange, const std::string &routingKey, int flags, const std::string &message); -bool publish(const std::string &exchange, const std::string &routingKey, const std::string &message); -bool publish(const std::string &exchange, const std::string &routingKey, int flags, const char *message, size_t size); -bool publish(const std::string &exchange, const std::string &routingKey, const char *message, size_t size); +DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const Envelope &envelope, int flags = 0) { return _implementation->publish(exchange, routingKey, envelope, flags); } +DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const std::string &message, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message.data(), message.size()), flags); } +DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const char *message, size_t size, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message, size), flags); } +DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const char *message, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message, strlen(message)), flags); } ```` Published messages are normally not confirmed by the server, and the RabbitMQ will not send a report back to inform you whether the message was succesfully -published or not. Therefore the publish method does not return a Deferred -object. +published or not. But with the flags you can instruct RabbitMQ to send back +the message if it was undeliverable. -As long as no error is reported via the Channel::onError() method, you can safely -assume that your messages were delivered. - -This can of course be a problem when you are publishing many messages. If you get +You can also use transactions to ensure that your messages get delivered. +Let's say that you are publishing many messages in a row. If you get an error halfway through there is no way to know for sure how many messages made it to the broker and how many should be republished. If this is important, you can wrap the publish commands inside a transaction. In this case, if an error occurs, @@ -774,8 +969,8 @@ channel.commitTransaction() }); ```` -Note that AMQP transactions are not as powerful as transactions that are -knows in the database world. It is not possible to wrap all sort of +Note that AMQP transactions are not as powerful as transactions that are +knows in the database world. It is not possible to wrap all sort of operations in a transaction, they are only meaningful for publishing and consuming. @@ -951,7 +1146,6 @@ need additional attention: - ability to set up secure connections (or is this fully done on the IO level) - login with other protocols than login/password - - returned messages We also need to add more safety checks so that strange or invalid data from RabbitMQ does not break the library (although in reality RabbitMQ only sends @@ -964,4 +1158,3 @@ class that can be directly plugged into libev, libevent and libuv event loops. For performance reasons, we need to investigate if we can limit the number of times an incoming or outgoing messages is copied. - diff --git a/amqpcpp.h b/amqpcpp.h deleted file mode 100644 index a1ac8d5..0000000 --- a/amqpcpp.h +++ /dev/null @@ -1,77 +0,0 @@ -/** - * AMQP.h - * - * Starting point for all includes of the Copernica AMQP library - * - * @documentation public - */ - -#pragma once - -// base C++ include files -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// base C include files -#include -#include - -// forward declarations -#include - -// utility classes -#include -#include -#include -#include -#include -#include -#include - -// amqp types -#include -#include -#include -#include -#include -#include -#include -#include - -// envelope for publishing and consuming -#include -#include -#include - -// mid level includes -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..5fa95c7 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,19 @@ +version: '1.0.{build}' + +image: Visual Studio 2017 + +platform: + - x64 + +configuration: + - Release + - Debug + +install: + - git submodule update --init --recursive + +before_build: + - cmake -G "Visual Studio 15 2017 Win64" . + +build: + project: $(APPVEYOR_BUILD_FOLDER)\amqpcpp.sln \ No newline at end of file diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..cba7efc --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +a.out diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..edf6358 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,32 @@ + + +################################### +# Boost +################################### + +add_executable(amqpcpp_boost_example libboostasio.cpp) + +add_dependencies(amqpcpp_boost_example amqpcpp) + +target_link_libraries(amqpcpp_boost_example amqpcpp boost_system pthread dl ssl) + +################################### +# Libev +################################### + +add_executable(amqpcpp_libev_example libev.cpp) + +add_dependencies(amqpcpp_libev_example amqpcpp) + +target_link_libraries(amqpcpp_libev_example amqpcpp ev pthread dl ssl) + + +################################### +# Libuv +################################### + +add_executable(amqpcpp_libuv_example libuv.cpp) + +add_dependencies(amqpcpp_libuv_example amqpcpp) + +target_link_libraries(amqpcpp_libuv_example amqpcpp uv pthread dl ssl) diff --git a/examples/libboostasio.cpp b/examples/libboostasio.cpp new file mode 100644 index 0000000..d290382 --- /dev/null +++ b/examples/libboostasio.cpp @@ -0,0 +1,56 @@ +/** + * LibBoostAsio.cpp + * + * Test program to check AMQP functionality based on Boost's asio io_service. + * + * @author Gavin Smith + * + * Compile with g++ -std=c++14 libboostasio.cpp -o boost_test -lpthread -lboost_system -lamqpcpp + */ + +/** + * Dependencies + */ +#include +#include +#include + + +#include +#include + +/** + * Main program + * @return int + */ +int main() +{ + + // access to the boost asio handler + // note: we suggest use of 2 threads - normally one is fin (we are simply demonstrating thread safety). + boost::asio::io_service service(4); + + // handler for libev + AMQP::LibBoostAsioHandler handler(service); + + // make a connection + AMQP::TcpConnection connection(&handler, AMQP::Address("amqp://guest:guest@localhost/")); + + // we need a channel too + AMQP::TcpChannel channel(&connection); + + // create a temporary queue + channel.declareQueue(AMQP::exclusive).onSuccess([&connection](const std::string &name, uint32_t messagecount, uint32_t consumercount) { + + // report the name of the temporary queue + std::cout << "declared queue " << name << std::endl; + + // now we can close the connection + connection.close(); + }); + + // run the handler + // a t the moment, one will need SIGINT to stop. In time, should add signal handling through boost API. + return service.run(); +} + diff --git a/examples/libev.cpp b/examples/libev.cpp new file mode 100644 index 0000000..7bfd1af --- /dev/null +++ b/examples/libev.cpp @@ -0,0 +1,107 @@ +/** + * LibEV.cpp + * + * Test program to check AMQP functionality based on LibEV + * + * @author Emiel Bruijntjes + * @copyright 2015 - 2018 Copernica BV + */ + +/** + * Dependencies + */ +#include +#include +#include +#include +#include + +/** + * Custom handler + */ +class MyHandler : public AMQP::LibEvHandler +{ +private: + /** + * Method that is called when a connection error occurs + * @param connection + * @param message + */ + virtual void onError(AMQP::TcpConnection *connection, const char *message) override + { + std::cout << "error: " << message << std::endl; + } + + /** + * Method that is called when the TCP connection ends up in a connected state + * @param connection The TCP connection + */ + virtual void onConnected(AMQP::TcpConnection *connection) override + { + std::cout << "connected" << std::endl; + } + +public: + /** + * Constructor + * @param ev_loop + */ + MyHandler(struct ev_loop *loop) : AMQP::LibEvHandler(loop) {} + + /** + * Destructor + */ + virtual ~MyHandler() = default; +}; + +/** + * Main program + * @return int + */ +int main() +{ + // access to the event loop + auto *loop = EV_DEFAULT; + + // handler for libev + MyHandler handler(loop); + + // init the SSL library +#if OPENSSL_VERSION_NUMBER < 0x10100000L + SSL_library_init(); +#else + OPENSSL_init_ssl(0, NULL); +#endif + + // make a connection + AMQP::Address address("amqp://guest:guest@localhost/"); +// AMQP::Address address("amqps://guest:guest@localhost/"); + AMQP::TcpConnection connection(&handler, address); + + // we need a channel too + AMQP::TcpChannel channel(&connection); + + // create a temporary queue + channel.declareQueue(AMQP::exclusive).onSuccess([&connection, &channel](const std::string &name, uint32_t messagecount, uint32_t consumercount) { + + // report the name of the temporary queue + std::cout << "declared queue " << name << std::endl; + + // close the channel + channel.close().onSuccess([&connection, &channel]() { + + // report that channel was closed + std::cout << "channel closed" << std::endl; + + // close the connection + connection.close(); + }); + }); + + // run the loop + ev_run(loop, 0); + + // done + return 0; +} + diff --git a/tests/libevent.cpp b/examples/libevent.cpp similarity index 100% rename from tests/libevent.cpp rename to examples/libevent.cpp diff --git a/examples/libuv.cpp b/examples/libuv.cpp new file mode 100644 index 0000000..8963eae --- /dev/null +++ b/examples/libuv.cpp @@ -0,0 +1,86 @@ +/** + * LibUV.cpp + * + * Test program to check AMQP functionality based on LibUV + * + * @author Emiel Bruijntjes + * @copyright 2015 - 2017 Copernica BV + */ + +/** + * Dependencies + */ +#include +#include +#include + +/** + * Custom handler + */ +class MyHandler : public AMQP::LibUvHandler +{ +private: + /** + * Method that is called when a connection error occurs + * @param connection + * @param message + */ + virtual void onError(AMQP::TcpConnection *connection, const char *message) override + { + std::cout << "error: " << message << std::endl; + } + + /** + * Method that is called when the TCP connection ends up in a connected state + * @param connection The TCP connection + */ + virtual void onConnected(AMQP::TcpConnection *connection) override + { + std::cout << "connected" << std::endl; + } + +public: + /** + * Constructor + * @param uv_loop + */ + MyHandler(uv_loop_t *loop) : AMQP::LibUvHandler(loop) {} + + /** + * Destructor + */ + virtual ~MyHandler() = default; +}; + +/** + * Main program + * @return int + */ +int main() +{ + // access to the event loop + auto *loop = uv_default_loop(); + + // handler for libev + MyHandler handler(loop); + + // make a connection + AMQP::TcpConnection connection(&handler, AMQP::Address("amqp://guest:guest@localhost/")); + + // we need a channel too + AMQP::TcpChannel channel(&connection); + + // create a temporary queue + channel.declareQueue(AMQP::exclusive).onSuccess([&connection](const std::string &name, uint32_t messagecount, uint32_t consumercount) { + + // report the name of the temporary queue + std::cout << "declared queue " << name << std::endl; + }); + + // run the loop + uv_run(loop, UV_RUN_DEFAULT); + + // done + return 0; +} + diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt deleted file mode 100644 index 455d1e6..0000000 --- a/include/CMakeLists.txt +++ /dev/null @@ -1,53 +0,0 @@ -add_sources( -address.h -addresses.h -array.h -booleanset.h -buffer.h -bytebuffer.h -callbacks.h -channel.h -channelimpl.h -classes.h -connection.h -connectionhandler.h -connectionimpl.h -copiedbuffer.h -decimalfield.h -deferred.h -deferredcancel.h -deferredconsumer.h -deferredconsumerbase.h -deferreddelete.h -deferredget.h -deferredqueue.h -endian.h -entityimpl.h -envelope.h -exception.h -exchangetype.h -field.h -fieldproxy.h -flags.h -frame.h -libev.h -libevent.h -libuv.h -login.h -message.h -metadata.h -monitor.h -numericfield.h -outbuffer.h -protocolexception.h -receivedframe.h -stack_ptr.h -stringfield.h -table.h -tcpchannel.h -tcpconnection.h -tcpdefines.h -tcphandler.h -watchable.h - -) diff --git a/include/amqpcpp.h b/include/amqpcpp.h new file mode 100644 index 0000000..8079b1f --- /dev/null +++ b/include/amqpcpp.h @@ -0,0 +1,82 @@ +/** + * AMQP.h + * + * Starting point for all includes of the Copernica AMQP library + * + * @author Emiel Bruijntjes + * @copyright 2015 - 2018 Copernica BV + */ + +#pragma once + +// base C++ include files +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// base C include files +#include +#include + +// fix strcasecmp on non linux platforms +#if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64) || defined(__WINDOWS__)) && !defined(__CYGWIN__) + #define strcasecmp _stricmp +#endif + +// forward declarations +#include "amqpcpp/classes.h" + +// utility classes +#include "amqpcpp/endian.h" +#include "amqpcpp/buffer.h" +#include "amqpcpp/bytebuffer.h" +#include "amqpcpp/receivedframe.h" +#include "amqpcpp/outbuffer.h" +#include "amqpcpp/watchable.h" +#include "amqpcpp/monitor.h" + +// amqp types +#include "amqpcpp/field.h" +#include "amqpcpp/numericfield.h" +#include "amqpcpp/decimalfield.h" +#include "amqpcpp/stringfield.h" +#include "amqpcpp/booleanset.h" +#include "amqpcpp/fieldproxy.h" +#include "amqpcpp/table.h" +#include "amqpcpp/array.h" + +// envelope for publishing and consuming +#include "amqpcpp/metadata.h" +#include "amqpcpp/envelope.h" +#include "amqpcpp/message.h" + +// mid level includes +#include "amqpcpp/exchangetype.h" +#include "amqpcpp/flags.h" +#include "amqpcpp/callbacks.h" +#include "amqpcpp/deferred.h" +#include "amqpcpp/deferredconsumer.h" +#include "amqpcpp/deferredqueue.h" +#include "amqpcpp/deferreddelete.h" +#include "amqpcpp/deferredcancel.h" +#include "amqpcpp/deferredget.h" +#include "amqpcpp/deferredpublisher.h" +#include "amqpcpp/channelimpl.h" +#include "amqpcpp/channel.h" +#include "amqpcpp/login.h" +#include "amqpcpp/address.h" +#include "amqpcpp/connectionhandler.h" +#include "amqpcpp/connectionimpl.h" +#include "amqpcpp/connection.h" +#include "amqpcpp/openssl.h" diff --git a/include/address.h b/include/amqpcpp/address.h similarity index 63% rename from include/address.h rename to include/amqpcpp/address.h index 2117995..e9bab9a 100644 --- a/include/address.h +++ b/include/amqpcpp/address.h @@ -4,9 +4,9 @@ * An AMQP address in the "amqp://user:password@hostname:port/vhost" notation * * @author Emiel Bruijntjes - * @copyright 2015 Copernica BV + * @copyright 2015 - 2018 Copernica BV */ - + /** * Include guard */ @@ -23,6 +23,12 @@ namespace AMQP { class Address { private: + /** + * The auth method + * @var bool + */ + bool _secure = false; + /** * Login data (username + password) * @var Login @@ -46,6 +52,16 @@ private: * @var std::string */ std::string _vhost; + + + /** + * The default port + * @return uint16_t + */ + uint16_t defaultport() const + { + return _secure ? 5671 : 5672; + } public: /** @@ -60,11 +76,17 @@ public: // position of the last byte const char *last = data + size; - // must start with amqp:// - if (size < 7 || strncmp(data, "amqp://", 7) != 0) throw std::runtime_error("AMQP address should start with \"amqp://\""); + // must start with ampqs:// to have a secure connection (and we also assign a different default port) + if (strncmp(data, "amqps://", 8) == 0) _secure = true; + + // otherwise protocol must be amqp:// + else if (strncmp(data, "amqp://", 7) != 0) throw std::runtime_error("AMQP address should start with \"amqp://\" or \"amqps://\""); + + // assign default port (we may overwrite it later) + _port = defaultport(); // begin of the string was parsed - data += 7; + data += _secure ? 8 : 7; // do we have a '@' to split user-data and hostname? const char *at = (const char *)memchr(data, '@', last - data); @@ -115,7 +137,7 @@ public: /** * Constructor to parse an address string - * The address should start with "amqp:// + * The address should start with amqp:// or amqps:// * @param data * @throws std::runtime_error */ @@ -133,8 +155,10 @@ public: * @param port * @param login * @param vhost + * @param secure */ - Address(std::string host, uint16_t port, Login login, std::string vhost) : + Address(std::string host, uint16_t port, Login login, std::string vhost, bool secure = false) : + _secure(secure), _login(std::move(login)), _hostname(std::move(host)), _port(port), @@ -145,6 +169,15 @@ public: */ virtual ~Address() = default; + /** + * Should we open a secure connection? + * @return bool + */ + bool secure() const + { + return _secure; + } + /** * Expose the login data * @return Login @@ -183,12 +216,12 @@ public: /** * Cast to a string - * @return std:string + * @return std::string */ operator std::string () const { // result object - std::string str("amqp://"); + std::string str(_secure ? "amqps://" : "amqp://"); // append login str.append(_login.user()).append(":").append(_login.password()).append("@").append(_hostname); @@ -213,10 +246,13 @@ public: */ bool operator==(const Address &that) const { + // security setting should match + if (_secure != that._secure) return false; + // logins must match if (_login != that._login) return false; - // hostname must match, but are nt case sensitive + // hostname must match, but are not case sensitive if (strcasecmp(_hostname.data(), that._hostname.data()) != 0) return false; // portnumber must match @@ -236,6 +272,62 @@ public: // the opposite of operator== return !operator==(that); } + + /** + * Comparison operator that is useful if addresses have to be ordered + * @param that + * @return bool + */ + bool operator<(const Address &that) const + { + // compare auth methods (amqp comes before amqps) + if (_secure != that._secure) return !_secure; + + // compare logins + if (_login != that._login) return _login < that._login; + + // hostname must match, but are not case sensitive + int result = strcasecmp(_hostname.data(), that._hostname.data()); + + // if hostnames are not equal, we know the result + if (result != 0) return result < 0; + + // portnumber must match + if (_port != that._port) return _port < that._port; + + // and finally compare the vhosts + return _vhost < that._vhost; + } + + /** + * Friend function to allow writing the address to a stream + * @param stream + * @param address + * @return std::ostream + */ + friend std::ostream &operator<<(std::ostream &stream, const Address &address) + { + // start with the protocol and login + stream << (address._secure ? "amqps://" : "amqp://"); + + // do we have a login? + if (address._login) stream << address._login << "@"; + + // write hostname + stream << address._hostname; + + // do we need a special portnumber? + if (address._port != address.defaultport()) stream << ":" << address._port; + + // append default vhost + stream << "/"; + + // do we have a special vhost? + if (address._vhost != "/") stream << address._vhost; + + // done + return stream; + } }; /** diff --git a/include/addresses.h b/include/amqpcpp/addresses.h similarity index 100% rename from include/addresses.h rename to include/amqpcpp/addresses.h diff --git a/include/array.h b/include/amqpcpp/array.h similarity index 100% rename from include/array.h rename to include/amqpcpp/array.h diff --git a/include/booleanset.h b/include/amqpcpp/booleanset.h similarity index 100% rename from include/booleanset.h rename to include/amqpcpp/booleanset.h diff --git a/include/buffer.h b/include/amqpcpp/buffer.h similarity index 100% rename from include/buffer.h rename to include/amqpcpp/buffer.h diff --git a/include/bytebuffer.h b/include/amqpcpp/bytebuffer.h similarity index 100% rename from include/bytebuffer.h rename to include/amqpcpp/bytebuffer.h diff --git a/include/amqpcpp/callbacks.h b/include/amqpcpp/callbacks.h new file mode 100644 index 0000000..76ac090 --- /dev/null +++ b/include/amqpcpp/callbacks.h @@ -0,0 +1,90 @@ +/** + * Callbacks.h + * + * Class storing deferred callbacks of different type. + * + * @copyright 2014 - 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Dependencies + */ +#include +#include + +/** + * Set up namespace + */ +namespace AMQP { + +/** + * Forward declarations + */ +class Message; +class MetaData; + +/** + * Generic callbacks that are used by many deferred objects + */ +using SuccessCallback = std::function; +using ErrorCallback = std::function; +using FinalizeCallback = std::function; + +/** + * Declaring and deleting a queue + */ +using QueueCallback = std::function; +using DeleteCallback = std::function; + +/** + * When retrieving the size of a queue in some way + */ +using EmptyCallback = std::function; +using SizeCallback = std::function; + +/** + * Starting and stopping a consumer + */ +using ConsumeCallback = std::function; +using CancelCallback = std::function; + +/** + * Receiving messages, either via consume(), get() or as returned messages + * The following methods receive the returned message in multiple parts + */ +using StartCallback = std::function; +using HeaderCallback = std::function; +using DataCallback = std::function; +using DeliveredCallback = std::function; + +/** + * For returned messages amqp-cpp first calls a return-callback before the start, + * header and data callbacks are called. Instead of the deliver-callback, a + * returned-callback is called. + */ +using ReturnCallback = std::function; +using ReturnedCallback = std::function; + +/** + * If you do not want to merge all data into a single string, you can als + * implement callbacks that return the collected message. + */ +using MessageCallback = std::function; +using BounceCallback = std::function; + +/** + * When using publisher confirms, AckCallback is called when server confirms that message is received + * and processed. NackCallback is called otherwise. + */ +using AckCallback = std::function; +using NackCallback = std::function; + +/** + * End namespace + */ +} diff --git a/include/channel.h b/include/amqpcpp/channel.h similarity index 89% rename from include/channel.h rename to include/amqpcpp/channel.h index 839ca02..3f70b40 100644 --- a/include/channel.h +++ b/include/amqpcpp/channel.h @@ -1,7 +1,7 @@ /** * Class describing a (mid-level) AMQP channel implementation * - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -29,6 +29,10 @@ private: public: /** * Construct a channel object + * + * The passed in connection pointer must remain valid for the + * lifetime of the channel. + * * @param connection */ Channel(Connection *connection) : _implementation(new ChannelImpl()) @@ -218,6 +222,7 @@ public: * - durable exchange survives a broker restart * - autodelete exchange is automatically removed when all connected queues are removed * - passive only check if the exchange exist + * - internal create an internal exchange * * @param name name of the exchange * @param type exchange type @@ -388,17 +393,42 @@ public: /** * Publish a message to an exchange - * + * + * You have to supply the name of an exchange and a routing key. RabbitMQ will then try + * to send the message to one or more queues. With the optional flags parameter you can + * specify what should happen if the message could not be routed to a queue. By default, + * unroutable message are silently discarded. + * + * This method returns a reference to a DeferredPublisher object. You can use this returned + * object to install callbacks that are called when an undeliverable message is returned, or + * to set the callback that is called when the server confirms that the message was received. + * + * To enable handling returned messages, or to enable publisher-confirms, you must not only + * set the callback, but also pass in appropriate flags to enable this feature. If you do not + * pass in these flags, your callbacks will not be called. If you are not at all interested + * in returned messages or publish-confirms, you can ignore the flag and the returned object. + * + * Watch out: the channel returns _the same_ DeferredPublisher object for all calls to the + * publish() method. This means that the callbacks that you install for the first published + * message are also used for subsequent messages _and_ it means that if you install a different + * callback for a later publish operation, it overwrites your earlier callbacks + * + * The following flags can be supplied: + * + * - mandatory If set, server returns messages that are not sent to a queue + * - immediate If set, server returns messages that can not immediately be forwarded to a consumer. + * * @param exchange the exchange to publish to * @param routingkey the routing key * @param envelope the full envelope to send * @param message the message to send * @param size size of the message + * @param flags optional flags */ - bool publish(const std::string &exchange, const std::string &routingKey, const Envelope &envelope) { return _implementation->publish(exchange, routingKey, envelope); } - bool publish(const std::string &exchange, const std::string &routingKey, const std::string &message) { return _implementation->publish(exchange, routingKey, Envelope(message.data(), message.size())); } - bool publish(const std::string &exchange, const std::string &routingKey, const char *message, size_t size) { return _implementation->publish(exchange, routingKey, Envelope(message, size)); } - bool publish(const std::string &exchange, const std::string &routingKey, const char *message) { return _implementation->publish(exchange, routingKey, Envelope(message, strlen(message))); } + DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const Envelope &envelope, int flags = 0) { return _implementation->publish(exchange, routingKey, envelope, flags); } + DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const std::string &message, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message.data(), message.size()), flags); } + DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const char *message, size_t size, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message, size), flags); } + DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const char *message, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message, strlen(message)), flags); } /** * Set the Quality of Service (QOS) for this channel diff --git a/include/channelimpl.h b/include/amqpcpp/channelimpl.h similarity index 90% rename from include/channelimpl.h rename to include/amqpcpp/channelimpl.h index 6770022..4229914 100644 --- a/include/channelimpl.h +++ b/include/amqpcpp/channelimpl.h @@ -5,7 +5,7 @@ * that has a private constructor so that it can not be used from outside * the AMQP library * - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -34,7 +34,7 @@ namespace AMQP { /** * Forward declarations */ -class DeferredConsumerBase; +class DeferredReceiver; class BasicDeliverFrame; class DeferredConsumer; class BasicGetOKFrame; @@ -44,6 +44,7 @@ class DeferredDelete; class DeferredCancel; class DeferredQueue; class DeferredGet; +class DeferredPublisher; class Connection; class Envelope; class Table; @@ -74,10 +75,16 @@ private: ErrorCallback _errorCallback; /** - * Handlers for all consumers that are active - * @var std::map + * Handler that deals with incoming messages as a result of publish operations + * @var std::shared_ptr */ - std::map> _consumers; + std::shared_ptr _publisher; + + /** + * Handlers for all consumers that are active + * @var std::map + */ + std::map> _consumers; /** * Pointer to the oldest deferred result (the first one that is going @@ -122,16 +129,17 @@ private: std::queue> _queue; /** - * Are we currently operating in synchronous mode? + * Are we currently operating in synchronous mode? Meaning: do we first have + * to wait for the answer to previous instructions before we send a new instruction? * @var bool */ bool _synchronous = false; /** - * The current consumer receiving a message - * @var std::shared_ptr + * The current object that is busy receiving a message + * @var std::shared_ptr */ - std::shared_ptr _consumer; + std::shared_ptr _receiver; /** * Number of messages sent. Used in confirm mode @@ -446,16 +454,17 @@ public: * Publish a message to an exchange * * If the mandatory or immediate flag is set, and the message could not immediately - * be published, the message will be returned to the client. However, the AMQP-CPP - * library does not yet report such returned messages. + * be published, the message will be returned to the client. * * @param exchange the exchange to publish to * @param routingkey the routing key * @param envelope the full envelope to send * @param message the message to send * @param size size of the message + * @param flags optional flags + * @return DeferredPublisher */ - bool publish(const std::string &exchange, const std::string &routingKey, const Envelope &envelope); + DeferredPublisher &publish(const std::string &exchange, const std::string &routingKey, const Envelope &envelope, int flags); /** * Set the Quality of Service (QOS) of the entire connection @@ -559,7 +568,7 @@ public: * @return bool */ bool reject(uint64_t deliveryTag, int flags); - + /** * Recover messages that were not yet ack'ed * @param flags optional flags @@ -634,6 +643,9 @@ public: // if we are still in connected state we are now ready if (_state == state_connected) _state = state_ready; + + // the last (possibly synchronous) operation was received, so we're no longer in synchronous mode + if (_synchronous && _queue.empty()) _synchronous = false; // inform handler if (_readyCallback) _readyCallback(); @@ -653,7 +665,6 @@ public: { // change state _state = state_closed; - _synchronous = false; // create a monitor, because the callbacks could destruct the current object Monitor monitor(this); @@ -687,6 +698,9 @@ public: { // skip if there is no oldest callback if (!_oldestCallback) return true; + + // the last (possibly synchronous) operation was received, so we're no longer in synchronous mode + if (_synchronous && _queue.empty()) _synchronous = false; // we are going to call callbacks that could destruct the channel Monitor monitor(this); @@ -720,18 +734,23 @@ public: /** * Install a consumer - * * @param consumertag The consumer tag - * @param consumer The consumer handler - * @param active Is this the new active consumer + * @param consumer The consumer object */ - void install(std::string consumertag, const std::shared_ptr &consumer, bool active = false) + void install(const std::string &consumertag, const std::shared_ptr &consumer) { // install the consumer handler _consumers[consumertag] = consumer; + } - // should we become the current consumer? - if (active) _consumer = consumer; + /** + * Install the current consumer + * @param receiver The receiver object + */ + void install(const std::shared_ptr &receiver) + { + // store object as current receiver + _receiver = receiver; } /** @@ -745,23 +764,23 @@ public: } /** - * Process incoming delivery - * - * @param frame The frame to process + * Fetch the receiver for a specific consumer tag + * @param consumertag the consumer tag + * @return the receiver object */ - void process(BasicDeliverFrame &frame); + DeferredConsumer *consumer(const std::string &consumertag) const; /** - * Retrieve the current consumer handler - * + * Retrieve the current object that is receiving a message * @return The handler responsible for the current message */ - DeferredConsumerBase *consumer(); - + DeferredReceiver *receiver() const { return _receiver.get(); } + /** - * Mark the current consumer as done + * Retrieve the deferred publisher that handles returned messages + * @return The deferred publisher object */ - void complete(); + DeferredPublisher *publisher() const { return _publisher.get(); } /** * Reset message counter diff --git a/include/classes.h b/include/amqpcpp/classes.h similarity index 100% rename from include/classes.h rename to include/amqpcpp/classes.h diff --git a/include/connection.h b/include/amqpcpp/connection.h similarity index 98% rename from include/connection.h rename to include/amqpcpp/connection.h index 18215f4..5fc1389 100644 --- a/include/connection.h +++ b/include/amqpcpp/connection.h @@ -94,6 +94,15 @@ public: return _implementation.vhost(); } + /** + * Send a ping/heartbeat to the channel to keep it alive + * @return bool + */ + bool heartbeat() + { + return _implementation.heartbeat(); + } + /** * Parse data that was recevied from RabbitMQ * @@ -195,15 +204,6 @@ public: return _implementation.waiting(); } - /** - * Retrieve the heartbeat delay used by this connection - * @return uint16_t - */ - uint16_t heartbeat() const - { - return _implementation.heartbeat(); - } - /** * Some classes have access to private properties */ diff --git a/include/connectionhandler.h b/include/amqpcpp/connectionhandler.h similarity index 94% rename from include/connectionhandler.h rename to include/amqpcpp/connectionhandler.h index e46fa8d..c68af2a 100644 --- a/include/connectionhandler.h +++ b/include/amqpcpp/connectionhandler.h @@ -51,6 +51,10 @@ public: * alternative heartbeat interval, you can override this method * to use an other interval. You should return 0 if you want to * disable heartbeats. + * + * If heartbeats are enabled, you yourself are responsible to send + * out a heartbeat every *interval* number of seconds by calling + * the Connection::heartbeat() method. * * @param connection The connection that suggested a heartbeat interval * @param interval The suggested interval from the server @@ -58,8 +62,8 @@ public: */ virtual uint16_t onNegotiate(Connection *connection, uint16_t interval) { - // default implementation, suggested heartbeat is ok - return interval; + // default implementation, disable heartbeats + return 0; } /** diff --git a/include/connectionimpl.h b/include/amqpcpp/connectionimpl.h similarity index 95% rename from include/connectionimpl.h rename to include/amqpcpp/connectionimpl.h index aaa8efa..3c805a9 100644 --- a/include/connectionimpl.h +++ b/include/amqpcpp/connectionimpl.h @@ -123,12 +123,12 @@ protected: * @var queue */ std::queue _queue; - + /** - * Heartbeat delay - * @var uint16_t + * Is the connection idle (meaning: a heartbeat is necessary) + * @var bool */ - uint16_t _heartbeat = 0; + bool _idle = true; /** * Helper method to send the close frame @@ -413,15 +413,6 @@ public: return _channels.size(); } - /** - * Heartbeat delay - * @return uint16_t - */ - uint16_t heartbeat() const - { - return _heartbeat; - } - /** * Set the heartbeat delay * @param heartbeat suggested heartbeat by server @@ -430,7 +421,7 @@ public: uint16_t setHeartbeat(uint16_t heartbeat) { // pass to the handler - return _heartbeat = _handler->onNegotiate(_parent, heartbeat); + return _handler->onNegotiate(_parent, heartbeat); } /** @@ -441,6 +432,14 @@ public: // pass to handler _handler->onHeartbeat(_parent); } + + /** + * Send a heartbeat to keep the connection alive + * By default, this function does nothing if the connection is not in an idle state + * @param force always send the heartbeat, even if the connection is not idle + * @return bool + */ + bool heartbeat(bool force=false); /** * The actual connection is a friend and can construct this class diff --git a/include/copiedbuffer.h b/include/amqpcpp/copiedbuffer.h similarity index 100% rename from include/copiedbuffer.h rename to include/amqpcpp/copiedbuffer.h diff --git a/include/decimalfield.h b/include/amqpcpp/decimalfield.h similarity index 100% rename from include/decimalfield.h rename to include/amqpcpp/decimalfield.h diff --git a/include/deferred.h b/include/amqpcpp/deferred.h similarity index 100% rename from include/deferred.h rename to include/amqpcpp/deferred.h diff --git a/include/deferredcancel.h b/include/amqpcpp/deferredcancel.h similarity index 100% rename from include/deferredcancel.h rename to include/amqpcpp/deferredcancel.h diff --git a/include/deferredconsumer.h b/include/amqpcpp/deferredconsumer.h similarity index 50% rename from include/deferredconsumer.h rename to include/amqpcpp/deferredconsumer.h index d5dd161..c8030e7 100644 --- a/include/deferredconsumer.h +++ b/include/amqpcpp/deferredconsumer.h @@ -3,7 +3,7 @@ * * Deferred callback for consumers * - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -14,17 +14,22 @@ /** * Dependencies */ -#include "deferredconsumerbase.h" +#include "deferredextreceiver.h" /** * Set up namespace */ namespace AMQP { + +/** + * Forward declararions + */ +class BasicDeliverFrame; /** * We extend from the default deferred and add extra functionality */ -class DeferredConsumer : public DeferredConsumerBase +class DeferredConsumer : public DeferredExtReceiver, public std::enable_shared_from_this { private: /** @@ -33,6 +38,13 @@ private: */ ConsumeCallback _consumeCallback; + /** + * Process a delivery frame + * + * @param frame The frame to process + */ + void process(BasicDeliverFrame &frame); + /** * Report success for frames that report start consumer operations * @param name Consumer tag that is started @@ -41,12 +53,10 @@ private: virtual const std::shared_ptr &reportSuccess(const std::string &name) override; /** - * Announce that a message has been received - * @param message The message to announce - * @param deliveryTag The delivery tag (for ack()ing) - * @param redelivered Is this a redelivered message + * Get reference to self to prevent that object falls out of scope + * @return std::shared_ptr */ - virtual void announce(const Message &message, uint64_t deliveryTag, bool redelivered) const override; + virtual std::shared_ptr lock() override { return shared_from_this(); } /** * The channel implementation may call our @@ -54,11 +64,11 @@ private: */ friend class ChannelImpl; friend class ConsumedMessage; + friend class BasicDeliverFrame; public: /** - * Protected constructor that can only be called - * from within the channel implementation + * Constructor that should only be called from within the channel implementation * * Note: this constructor _should_ be protected, but because make_shared * will then not work, we have decided to make it public after all, @@ -68,26 +78,136 @@ public: * @param failed are we already failed? */ DeferredConsumer(ChannelImpl *channel, bool failed = false) : - DeferredConsumerBase(failed, channel) {} + DeferredExtReceiver(failed, channel) {} public: /** - * Register the function to be called when a new message is expected - * - * @param callback The callback to invoke - * @return Same object for chaining + * Register a callback function that gets called when the consumer is + * started. In the callback you will for receive the consumer-tag + * that you need to later stop the consumer + * @param callback */ - DeferredConsumer &onBegin(const BeginCallback &callback) + DeferredConsumer &onSuccess(const ConsumeCallback &callback) { - // store callback - _beginCallback = callback; + // store the callback + _consumeCallback = callback; // allow chaining return *this; } /** - * Register the function to be called when message headers come in + * Register the function that is called when the consumer starts. + * It is recommended to use the onSuccess() method mentioned above + * since that will also pass the consumer-tag as parameter. + * @param callback + */ + DeferredConsumer &onSuccess(const SuccessCallback &callback) + { + // call base + Deferred::onSuccess(callback); + + // allow chaining + return *this; + } + + /** + * Register a function to be called when a full message is received + * @param callback the callback to execute + */ + DeferredConsumer &onReceived(const MessageCallback &callback) + { + // store callback + _messageCallback = callback; + + // allow chaining + return *this; + } + + /** + * Alias for onReceived() (see above) + * @param callback the callback to execute + */ + DeferredConsumer &onMessage(const MessageCallback &callback) + { + // store callback + _messageCallback = callback; + + // allow chaining + return *this; + } + + /** + * RabbitMQ sends a message in multiple frames to its consumers. + * The AMQP-CPP library collects these frames and merges them into a + * single AMQP::Message object that is passed to the callback that + * you can set with the onReceived() or onMessage() methods (see above). + * + * However, you can also write your own algorithm to merge the frames. + * In that case you can install callbacks to handle the frames. Every + * message is sent in a number of frames: + * + * - a begin frame that marks the start of the message + * - an optional header if the message was sent with an envelope + * - zero or more data frames (usually 1, but more for large messages) + * - an end frame to mark the end of the message. + * + * To install handlers for these frames, you can use the onBegin(), + * onHeaders(), onData() and onComplete() methods. + * + * If you just rely on the onReceived() or onMessage() callbacks, you + * do not need any of the methods below this line. + */ + + /** + * Register the function that is called when the start frame of a new + * consumed message is received + * + * @param callback The callback to invoke + * @return Same object for chaining + */ + DeferredConsumer &onBegin(const StartCallback &callback) + { + // store callback + _startCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function that is called when the start frame of a new + * consumed message is received + * + * @param callback The callback to invoke + * @return Same object for chaining + */ + DeferredConsumer &onStart(const StartCallback &callback) + { + // store callback + _startCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register a function that is called when the message size is known + * + * @param callback The callback to invoke for message headers + * @return Same object for chaining + */ + DeferredConsumer &onSize(const SizeCallback &callback) + { + // store callback + _sizeCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function that is called when message headers come in * * @param callback The callback to invoke for message headers * @return Same object for chaining @@ -124,54 +244,15 @@ public: } /** - * Register the function that is called when the consumer starts - * @param callback + * Register a funtion to be called when a message was completely received + * + * @param callback The callback to invoke + * @return Same object for chaining */ - DeferredConsumer &onSuccess(const ConsumeCallback &callback) - { - // store the callback - _consumeCallback = callback; - - // allow chaining - return *this; - } - - /** - * Register the function that is called when the consumer starts - * @param callback - */ - DeferredConsumer &onSuccess(const SuccessCallback &callback) - { - // call base - Deferred::onSuccess(callback); - - // allow chaining - return *this; - } - - /** - * Register a function to be called when a message arrives - * This fuction is also available as onMessage() because I always forget which name I gave to it - * @param callback the callback to execute - */ - DeferredConsumer &onReceived(const MessageCallback &callback) + DeferredConsumer &onComplete(const DeliveredCallback &callback) { // store callback - _messageCallback = callback; - - // allow chaining - return *this; - } - - /** - * Register a function to be called when a message arrives - * This fuction is also available as onReceived() because I always forget which name I gave to it - * @param callback the callback to execute - */ - DeferredConsumer &onMessage(const MessageCallback &callback) - { - // store callback - _messageCallback = callback; + _deliveredCallback = callback; // allow chaining return *this; @@ -183,10 +264,10 @@ public: * @param callback The callback to invoke * @return Same object for chaining */ - DeferredConsumer &onComplete(const CompleteCallback &callback) + DeferredConsumer &onDelivered(const DeliveredCallback &callback) { // store callback - _completeCallback = callback; + _deliveredCallback = callback; // allow chaining return *this; diff --git a/include/deferreddelete.h b/include/amqpcpp/deferreddelete.h similarity index 100% rename from include/deferreddelete.h rename to include/amqpcpp/deferreddelete.h diff --git a/include/amqpcpp/deferredextreceiver.h b/include/amqpcpp/deferredextreceiver.h new file mode 100644 index 0000000..3d427e3 --- /dev/null +++ b/include/amqpcpp/deferredextreceiver.h @@ -0,0 +1,91 @@ +/** + * DeferredExtReceiver.h + * + * Extended receiver that _wants_ to receive message (because it is + * consuming or get'ting messages. This is the base class for both + * the DeferredConsumer as well as the DeferredGet classes, but not + * the base of the DeferredPublisher (which can also receive returned + * messages, but not as a result of an explicit request) + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Dependencies + */ +#include "deferredreceiver.h" + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * Class definition + */ +class DeferredExtReceiver : public DeferredReceiver +{ +protected: + /** + * The delivery tag for the current message + * @var uint64_t + */ + uint64_t _deliveryTag = 0; + + /** + * Is this a redelivered message + * @var bool + */ + bool _redelivered = false; + + /** + * Callback for incoming messages + * @var MessageCallback + */ + MessageCallback _messageCallback; + + /** + * Callback for when a message was complete finished + * @var DeliveredCallback + */ + DeliveredCallback _deliveredCallback; + + + /** + * Initialize the object to send out a message + * @param exchange the exchange to which the message was published + * @param routingkey the routing key that was used to publish the message + */ + virtual void initialize(const std::string &exchange, const std::string &routingkey) override; + + /** + * Indicate that a message was done + */ + virtual void complete() override; + + /** + * Constructor + * @param failed Have we already failed? + * @param channel The channel we are consuming on + */ + DeferredExtReceiver(bool failed, ChannelImpl *channel) : + DeferredReceiver(failed, channel) {} + +public: + /** + * Destructor + */ + virtual ~DeferredExtReceiver() = default; +}; + +/** + * End of namespace + */ +} + diff --git a/include/deferredget.h b/include/amqpcpp/deferredget.h similarity index 73% rename from include/deferredget.h rename to include/amqpcpp/deferredget.h index 4270d34..78332be 100644 --- a/include/deferredget.h +++ b/include/amqpcpp/deferredget.h @@ -2,7 +2,7 @@ * DeferredGet.h * * @author Emiel Bruijntjes - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -13,7 +13,7 @@ /** * Dependencies */ -#include "deferredconsumerbase.h" +#include "deferredextreceiver.h" /** * Set up namespace @@ -27,7 +27,7 @@ namespace AMQP { * it grabs a self-pointer when the callback is running, otherwise the onFinalize() * is called before the actual message is consumed. */ -class DeferredGet : public DeferredConsumerBase +class DeferredGet : public DeferredExtReceiver, public std::enable_shared_from_this { private: /** @@ -40,7 +40,7 @@ private: * Callback with the number of messages still in the queue * @var SizeCallback */ - SizeCallback _sizeCallback; + SizeCallback _countCallback; /** * Report success for a get operation @@ -57,12 +57,15 @@ private: virtual const std::shared_ptr &reportSuccess() const override; /** - * Announce that a message has been received - * @param message The message to announce - * @param deliveryTag The delivery tag (for ack()ing) - * @param redelivered Is this a redelivered message + * Get reference to self to prevent that object falls out of scope + * @return std::shared_ptr */ - virtual void announce(const Message &message, uint64_t deliveryTag, bool redelivered) const override; + virtual std::shared_ptr lock() override { return shared_from_this(); } + + /** + * Extended implementation of the complete method that is called when a message was fully received + */ + virtual void complete() override; /** * The channel implementation may call our @@ -84,61 +87,9 @@ public: * @param failed are we already failed? */ DeferredGet(ChannelImpl *channel, bool failed = false) : - DeferredConsumerBase(failed, channel) {} + DeferredExtReceiver(failed, channel) {} public: - /** - * Register the function to be called when a new message is expected - * - * @param callback The callback to invoke - * @return Same object for chaining - */ - DeferredGet &onBegin(const BeginCallback &callback) - { - // store callback - _beginCallback = callback; - - // allow chaining - return *this; - } - - /** - * Register the function to be called when message headers come in - * - * @param callback The callback to invoke for message headers - * @return Same object for chaining - */ - DeferredGet &onHeaders(const HeaderCallback &callback) - { - // store callback - _headerCallback = callback; - - // allow chaining - return *this; - } - - /** - * Register the function to be called when a chunk of data comes in - * - * Note that this function may be called zero, one or multiple times - * for each incoming message depending on the size of the message data. - * - * If you install this callback you very likely also want to install - * the onComplete callback so you know when the last data part was - * received. - * - * @param callback The callback to invoke for chunks of message data - * @return Same object for chaining - */ - DeferredGet &onData(const DataCallback &callback) - { - // store callback - _dataCallback = callback; - - // allow chaining - return *this; - } - /** * Register a function to be called when a message arrives * This fuction is also available as onReceived() and onMessage() because I always forget which name I gave to it @@ -195,13 +146,95 @@ public: } /** - * Register a function to be called when size information is known + * Register a function to be called when queue size information is known * @param callback the callback to execute */ + DeferredGet &onCount(const SizeCallback &callback) + { + // store callback + _countCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function to be called when a new message is expected + * + * @param callback The callback to invoke + * @return Same object for chaining + */ + DeferredGet &onBegin(const StartCallback &callback) + { + // store callback + _startCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function to be called when a new message is expected + * + * @param callback The callback to invoke + * @return Same object for chaining + */ + DeferredGet &onStart(const StartCallback &callback) + { + // store callback + _startCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register a function that is called when the message size is known + * + * @param callback The callback to invoke for message headers + * @return Same object for chaining + */ DeferredGet &onSize(const SizeCallback &callback) { // store callback _sizeCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function to be called when message headers come in + * + * @param callback The callback to invoke for message headers + * @return Same object for chaining + */ + DeferredGet &onHeaders(const HeaderCallback &callback) + { + // store callback + _headerCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function to be called when a chunk of data comes in + * + * Note that this function may be called zero, one or multiple times + * for each incoming message depending on the size of the message data. + * + * If you install this callback you very likely also want to install + * the onComplete callback so you know when the last data part was + * received. + * + * @param callback The callback to invoke for chunks of message data + * @return Same object for chaining + */ + DeferredGet &onData(const DataCallback &callback) + { + // store callback + _dataCallback = callback; // allow chaining return *this; @@ -213,10 +246,25 @@ public: * @param callback The callback to invoke * @return Same object for chaining */ - DeferredGet &onComplete(const CompleteCallback &callback) + DeferredGet &onComplete(const DeliveredCallback &callback) { // store callback - _completeCallback = callback; + _deliveredCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register a funtion to be called when a message was completely received + * + * @param callback The callback to invoke + * @return Same object for chaining + */ + DeferredGet &onDelivered(const DeliveredCallback &callback) + { + // store callback + _deliveredCallback = callback; // allow chaining return *this; diff --git a/include/amqpcpp/deferredpublisher.h b/include/amqpcpp/deferredpublisher.h new file mode 100644 index 0000000..3c9fb17 --- /dev/null +++ b/include/amqpcpp/deferredpublisher.h @@ -0,0 +1,241 @@ +/** + * DeferredPublisher.h + * + * Class that is returned when channel::publish() is called, and that + * can be used to install callback methods that define how returned + * messages should be handled. + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * Forward declarations + */ +class ChannelImpl; + +/** + * Class definition + */ +class DeferredPublisher : public DeferredReceiver, public std::enable_shared_from_this +{ +private: + /** + * The error code + * @var int16_t + */ + int16_t _code = 0; + + /** + * The error message + * @var std::string + */ + std::string _description; + + /** + * Callback that is called when a message is returned + * @var BounceCallback + */ + BounceCallback _bounceCallback; + + /** + * Begin of a bounced message + * @var ReturnCallback + */ + ReturnCallback _beginCallback; + + /** + * End of a bounced message + * @var ReturnedCallback + */ + ReturnedCallback _completeCallback; + + /** + * Process a return frame + * + * @param frame The frame to process + */ + void process(BasicReturnFrame &frame); + + /** + * Get reference to self to prevent that object falls out of scope + * @return std::shared_ptr + */ + virtual std::shared_ptr lock() override { return shared_from_this(); } + + /** + * Extended implementation of the complete method that is called when a message was fully received + */ + virtual void complete() override; + + /** + * Classes that can access private members + */ + friend class BasicReturnFrame; + +public: + /** + * Constructor that should only be called from within the channel implementation + * + * Note: this constructor _should_ be protected, but because make_shared + * will then not work, we have decided to make it public after all, + * because the work-around would result in not-so-easy-to-read code. + * + * @param channel the channel implementation + * @param failed are we already failed? + */ + DeferredPublisher(ChannelImpl *channel, bool failed = false) : + DeferredReceiver(failed, channel) {} + +public: + /** + * Register a function to be called when a full message is returned + * @param callback the callback to execute + */ + DeferredPublisher &onReceived(const BounceCallback &callback) + { + // store callback + _bounceCallback = callback; + + // allow chaining + return *this; + } + + /** + * Alias for onReceived() (see above) + * @param callback the callback to execute + */ + DeferredPublisher &onMessage(const BounceCallback &callback) + { + // store callback + _bounceCallback = callback; + + // allow chaining + return *this; + } + + /** + * Alias for onReceived() (see above) + * @param callback the callback to execute + */ + DeferredPublisher &onReturned(const BounceCallback &callback) + { + // store callback + _bounceCallback = callback; + + // allow chaining + return *this; + } + + /** + * Alias for onReceived() (see above) + * @param callback the callback to execute + */ + DeferredPublisher &onBounced(const BounceCallback &callback) + { + // store callback + _bounceCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function that is called when the start frame of a new + * consumed message is received + * + * @param callback The callback to invoke + * @return Same object for chaining + */ + DeferredPublisher &onBegin(const ReturnCallback &callback) + { + // store callback + _beginCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register a function that is called when the message size is known + * + * @param callback The callback to invoke for message headers + * @return Same object for chaining + */ + DeferredPublisher &onSize(const SizeCallback &callback) + { + // store callback + _sizeCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function that is called when message headers come in + * + * @param callback The callback to invoke for message headers + * @return Same object for chaining + */ + DeferredPublisher &onHeaders(const HeaderCallback &callback) + { + // store callback + _headerCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register the function to be called when a chunk of data comes in + * + * Note that this function may be called zero, one or multiple times + * for each incoming message depending on the size of the message data. + * + * If you install this callback you very likely also want to install + * the onComplete callback so you know when the last data part was + * received. + * + * @param callback The callback to invoke for chunks of message data + * @return Same object for chaining + */ + DeferredPublisher &onData(const DataCallback &callback) + { + // store callback + _dataCallback = callback; + + // allow chaining + return *this; + } + + /** + * Register a funtion to be called when a message was completely received + * + * @param callback The callback to invoke + * @return Same object for chaining + */ + DeferredPublisher &onComplete(const ReturnedCallback &callback) + { + // store callback + _completeCallback = callback; + + // allow chaining + return *this; + } +}; + +/** + * End of namespace + */ +} + diff --git a/include/deferredqueue.h b/include/amqpcpp/deferredqueue.h similarity index 100% rename from include/deferredqueue.h rename to include/amqpcpp/deferredqueue.h diff --git a/include/deferredconsumerbase.h b/include/amqpcpp/deferredreceiver.h similarity index 54% rename from include/deferredconsumerbase.h rename to include/amqpcpp/deferredreceiver.h index 5b3bbdb..caa489b 100644 --- a/include/deferredconsumerbase.h +++ b/include/amqpcpp/deferredreceiver.h @@ -1,10 +1,10 @@ /** - * deferredconsumerbase.h + * DeferredReceiver.h * - * Base class for the deferred consumer and the - * deferred get. + * Base class for the deferred consumer, the deferred get and the + * deferred publisher (that may receive returned messages) * - * @copyright 2016 - 2017 Copernica B.V. + * @copyright 2016 - 2018 Copernica B.V. */ /** @@ -35,9 +35,7 @@ class BodyFrame; /** * Base class for deferred consumers */ -class DeferredConsumerBase : - public Deferred, - public std::enable_shared_from_this +class DeferredReceiver : public Deferred { private: /** @@ -46,20 +44,27 @@ private: */ uint64_t _bodySize = 0; - /** - * Process a delivery frame - * - * @param frame The frame to process - */ - void process(BasicDeliverFrame &frame); +protected: /** - * Process a delivery frame from a get request - * - * @param frame The frame to process + * Initialize the object to send out a message + * @param exchange the exchange to which the message was published + * @param routingkey the routing key that was used to publish the message */ - void process(BasicGetOKFrame &frame); + virtual void initialize(const std::string &exchange, const std::string &routingkey); + + /** + * Get reference to self to prevent that object falls out of scope + * @return std::shared_ptr + */ + virtual std::shared_ptr lock() = 0; + + /** + * Indicate that a message was done + */ + virtual void complete() = 0; +private: /** * Process the message headers * @@ -74,40 +79,15 @@ private: */ void process(BodyFrame &frame); - /** - * Indicate that a message was done - */ - void complete(); - - /** - * Announce that a message has been received - * @param message The message to announce - * @param deliveryTag The delivery tag (for ack()ing) - * @param redelivered Is this a redelivered message - */ - virtual void announce(const Message &message, uint64_t deliveryTag, bool redelivered) const = 0; - /** * Frames may be processed */ friend class ChannelImpl; - friend class BasicDeliverFrame; friend class BasicGetOKFrame; friend class BasicHeaderFrame; friend class BodyFrame; + protected: - /** - * The delivery tag for the current message - * @var uint64_t - */ - uint64_t _deliveryTag = 0; - - /** - * Is this a redelivered message - * @var bool - */ - bool _redelivered = false; - /** * The channel to which the consumer is linked * @var ChannelImpl @@ -116,9 +96,15 @@ protected: /** * Callback for new message - * @var BeginCallback + * @var StartCallback */ - BeginCallback _beginCallback; + StartCallback _startCallback; + + /** + * Callback that is called when size of the message is known + * @var SizeCallback + */ + SizeCallback _sizeCallback; /** * Callback for incoming headers @@ -132,18 +118,6 @@ protected: */ DataCallback _dataCallback; - /** - * Callback for incoming messages - * @var MessageCallback - */ - MessageCallback _messageCallback; - - /** - * Callback for when a message was complete finished - * @var CompleteCallback - */ - CompleteCallback _completeCallback; - /** * The message that we are currently receiving * @var stack_ptr @@ -152,12 +126,17 @@ protected: /** * Constructor - * * @param failed Have we already failed? * @param channel The channel we are consuming on */ - DeferredConsumerBase(bool failed, ChannelImpl *channel) : Deferred(failed), _channel(channel) {} + DeferredReceiver(bool failed, ChannelImpl *channel) : + Deferred(failed), _channel(channel) {} + public: + /** + * Destructor + */ + virtual ~DeferredReceiver() = default; }; /** diff --git a/include/endian.h b/include/amqpcpp/endian.h similarity index 96% rename from include/endian.h rename to include/amqpcpp/endian.h index 8662464..72242a6 100644 --- a/include/endian.h +++ b/include/amqpcpp/endian.h @@ -58,7 +58,7 @@ #include #pragma comment(lib,"Ws2_32.lib") -//# include +//# include #if BYTE_ORDER == LITTLE_ENDIAN @@ -72,7 +72,7 @@ #define be32toh(x) ntohl(x) #define le32toh(x) (x) -#define htobe64(x) htonll(x) +#define htobe64(x) ((1==htonl(1)) ? (x) : ((uint64_t)htonl((x) & 0xFFFFFFFF) << 32) | htonl((x) >> 32)) #define htole64(x) (x) #define be64toh(x) ntohll(x) #define le64toh(x) (x) diff --git a/include/entityimpl.h b/include/amqpcpp/entityimpl.h similarity index 100% rename from include/entityimpl.h rename to include/amqpcpp/entityimpl.h diff --git a/include/envelope.h b/include/amqpcpp/envelope.h similarity index 100% rename from include/envelope.h rename to include/amqpcpp/envelope.h diff --git a/include/exception.h b/include/amqpcpp/exception.h similarity index 100% rename from include/exception.h rename to include/amqpcpp/exception.h diff --git a/include/exchangetype.h b/include/amqpcpp/exchangetype.h similarity index 100% rename from include/exchangetype.h rename to include/amqpcpp/exchangetype.h diff --git a/include/field.h b/include/amqpcpp/field.h similarity index 100% rename from include/field.h rename to include/amqpcpp/field.h diff --git a/include/fieldproxy.h b/include/amqpcpp/fieldproxy.h similarity index 100% rename from include/fieldproxy.h rename to include/amqpcpp/fieldproxy.h diff --git a/include/flags.h b/include/amqpcpp/flags.h similarity index 91% rename from include/flags.h rename to include/amqpcpp/flags.h index 3a4a2c3..37779af 100644 --- a/include/flags.h +++ b/include/amqpcpp/flags.h @@ -3,7 +3,7 @@ * * The various flags that are supported * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -38,6 +38,7 @@ extern const int multiple; extern const int requeue; extern const int readable; extern const int writable; +extern const int internal; /** * End of namespace diff --git a/include/frame.h b/include/amqpcpp/frame.h similarity index 100% rename from include/frame.h rename to include/amqpcpp/frame.h diff --git a/include/amqpcpp/libboostasio.h b/include/amqpcpp/libboostasio.h new file mode 100644 index 0000000..c1492f5 --- /dev/null +++ b/include/amqpcpp/libboostasio.h @@ -0,0 +1,602 @@ +/** + * LibBoostAsio.h + * + * Implementation for the AMQP::TcpHandler for boost::asio. You can use this class + * instead of a AMQP::TcpHandler class, just pass the boost asio service to the + * constructor and you're all set. See tests/libboostasio.cpp for example. + * + * Watch out: this class was not implemented or reviewed by the original author of + * AMQP-CPP. However, we do get a lot of questions and issues from users of this class, + * so we cannot guarantee its quality. If you run into such issues too, it might be + * better to implement your own handler that interact with boost. + * + * + * @author Gavin Smith + */ + + +/** + * Include guard + */ +#pragma once + +/** + * Dependencies + */ +#include + +#include +#include +#include +#include +#include +#include + +#include "amqpcpp/linux_tcp.h" + +// C++17 has 'weak_from_this()' support. +#if __cplusplus >= 201701L +#define PTR_FROM_THIS(T) weak_from_this() +#else +#define PTR_FROM_THIS(T) std::weak_ptr(shared_from_this()) +#endif + +/** + * Set up namespace + */ +namespace AMQP { + +/** + * Class definition + * @note Because of a limitation on Windows, this will only work on POSIX based systems - see https://github.com/chriskohlhoff/asio/issues/70 + */ +class LibBoostAsioHandler : public virtual TcpHandler +{ +protected: + + /** + * Helper class that wraps a boost io_service socket monitor. + */ + class Watcher : public virtual std::enable_shared_from_this + { + private: + + /** + * The boost asio io_service which is responsible for detecting events. + * @var class boost::asio::io_service& + */ + boost::asio::io_service & _ioservice; + + using strand_weak_ptr = std::weak_ptr; + + /** + * The boost asio io_service::strand managed pointer. + * @var class std::shared_ptr + */ + strand_weak_ptr _wpstrand; + + /** + * The boost tcp socket. + * @var class boost::asio::ip::tcp::socket + * @note https://stackoverflow.com/questions/38906711/destroying-boost-asio-socket-without-closing-native-handler + */ + boost::asio::posix::stream_descriptor _socket; + + /** + * A boolean that indicates if the watcher is monitoring for read events. + * @var _read True if reads are being monitored else false. + */ + bool _read{false}; + + /** + * A boolean that indicates if the watcher has a pending read event. + * @var _read True if read is pending else false. + */ + bool _read_pending{false}; + + /** + * A boolean that indicates if the watcher is monitoring for write events. + * @var _read True if writes are being monitored else false. + */ + bool _write{false}; + + /** + * A boolean that indicates if the watcher has a pending write event. + * @var _read True if read is pending else false. + */ + bool _write_pending{false}; + + using handler_cb = boost::function; + using io_handler = boost::function; + + /** + * Builds a io handler callback that executes the io callback in a strand. + * @param io_handler The handler callback to dispatch + * @return handler_cb A function wrapping the execution of the handler function in a io_service::strand. + */ + handler_cb get_dispatch_wrapper(io_handler fn) + { + const strand_weak_ptr wpstrand = _wpstrand; + + return [fn, wpstrand](const boost::system::error_code &ec, const std::size_t bytes_transferred) + { + const strand_shared_ptr strand = wpstrand.lock(); + if (!strand) + { + fn(boost::system::errc::make_error_code(boost::system::errc::operation_canceled), std::size_t{0}); + return; + } + strand->dispatch(boost::bind(fn, ec, bytes_transferred)); + }; + } + + /** + * Binds and returns a read handler for the io operation. + * @param connection The connection being watched. + * @param fd The file descripter being watched. + * @return handler callback + */ + handler_cb get_read_handler(TcpConnection *const connection, const int fd) + { + auto fn = boost::bind(&Watcher::read_handler, + this, + _1, + _2, + PTR_FROM_THIS(Watcher), + connection, + fd); + return get_dispatch_wrapper(fn); + } + + /** + * Binds and returns a read handler for the io operation. + * @param connection The connection being watched. + * @param fd The file descripter being watched. + * @return handler callback + */ + handler_cb get_write_handler(TcpConnection *const connection, const int fd) + { + auto fn = boost::bind(&Watcher::write_handler, + this, + _1, + _2, + PTR_FROM_THIS(Watcher), + connection, + fd); + return get_dispatch_wrapper(fn); + } + + /** + * Handler method that is called by boost's io_service when the socket pumps a read event. + * @param ec The status of the callback. + * @param bytes_transferred The number of bytes transferred. + * @param awpWatcher A weak pointer to this object. + * @param connection The connection being watched. + * @param fd The file descriptor being watched. + * @note The handler will get called if a read is cancelled. + */ + void read_handler(const boost::system::error_code &ec, + const std::size_t bytes_transferred, + const std::weak_ptr awpWatcher, + TcpConnection *const connection, + const int fd) + { + // Resolve any potential problems with dangling pointers + // (remember we are using async). + const std::shared_ptr apWatcher = awpWatcher.lock(); + if (!apWatcher) { return; } + + _read_pending = false; + + if ((!ec || ec == boost::asio::error::would_block) && _read) + { + connection->process(fd, AMQP::readable); + + _read_pending = true; + + _socket.async_read_some( + boost::asio::null_buffers(), + get_read_handler(connection, fd)); + } + } + + /** + * Handler method that is called by boost's io_service when the socket pumps a write event. + * @param ec The status of the callback. + * @param bytes_transferred The number of bytes transferred. + * @param awpWatcher A weak pointer to this object. + * @param connection The connection being watched. + * @param fd The file descriptor being watched. + * @note The handler will get called if a write is cancelled. + */ + void write_handler(const boost::system::error_code ec, + const std::size_t bytes_transferred, + const std::weak_ptr awpWatcher, + TcpConnection *const connection, + const int fd) + { + // Resolve any potential problems with dangling pointers + // (remember we are using async). + const std::shared_ptr apWatcher = awpWatcher.lock(); + if (!apWatcher) { return; } + + _write_pending = false; + + if ((!ec || ec == boost::asio::error::would_block) && _write) + { + connection->process(fd, AMQP::writable); + + _write_pending = true; + + _socket.async_write_some( + boost::asio::null_buffers(), + get_write_handler(connection, fd)); + } + } + + public: + /** + * Constructor- initialises the watcher and assigns the filedescriptor to + * a boost socket for monitoring. + * @param io_service The boost io_service + * @param wpstrand A weak pointer to a io_service::strand instance. + * @param fd The filedescriptor being watched + */ + Watcher(boost::asio::io_service &io_service, + const strand_weak_ptr wpstrand, + const int fd) : + _ioservice(io_service), + _wpstrand(wpstrand), + _socket(_ioservice) + { + _socket.assign(fd); + + _socket.non_blocking(true); + } + + /** + * Watchers cannot be copied or moved + * + * @param that The object to not move or copy + */ + Watcher(Watcher &&that) = delete; + Watcher(const Watcher &that) = delete; + + /** + * Destructor + */ + ~Watcher() + { + _read = false; + _write = false; + _socket.release(); + } + + /** + * Change the events for which the filedescriptor is monitored + * @param events + */ + void events(TcpConnection *connection, int fd, int events) + { + // 1. Handle reads? + _read = ((events & AMQP::readable) != 0); + + // Read requsted but no read pending? + if (_read && !_read_pending) + { + _read_pending = true; + + _socket.async_read_some( + boost::asio::null_buffers(), + get_read_handler(connection, fd)); + } + + // 2. Handle writes? + _write = ((events & AMQP::writable) != 0); + + // Write requested but no write pending? + if (_write && !_write_pending) + { + _write_pending = true; + + _socket.async_write_some( + boost::asio::null_buffers(), + get_write_handler(connection, fd)); + } + } + }; + + /** + * Timer class to periodically fire a heartbeat + */ + class Timer : public std::enable_shared_from_this + { + private: + + /** + * The boost asio io_service which is responsible for detecting events. + * @var class boost::asio::io_service& + */ + boost::asio::io_service & _ioservice; + + using strand_weak_ptr = std::weak_ptr; + + /** + * The boost asio io_service::strand managed pointer. + * @var class std::shared_ptr + */ + strand_weak_ptr _wpstrand; + + /** + * The boost asynchronous deadline timer. + * @var class boost::asio::deadline_timer + */ + boost::asio::deadline_timer _timer; + + using handler_fn = boost::function; + + /** + * Binds and returns a lamba function handler for the io operation. + * @param connection The connection being watched. + * @param timeout The file descripter being watched. + * @return handler callback + */ + handler_fn get_handler(TcpConnection *const connection, const uint16_t timeout) + { + const auto fn = boost::bind(&Timer::timeout, + this, + _1, + PTR_FROM_THIS(Timer), + connection, + timeout); + + const strand_weak_ptr wpstrand = _wpstrand; + + return [fn, wpstrand](const boost::system::error_code &ec) + { + const strand_shared_ptr strand = wpstrand.lock(); + if (!strand) + { + fn(boost::system::errc::make_error_code(boost::system::errc::operation_canceled)); + return; + } + strand->dispatch(boost::bind(fn, ec)); + }; + } + + /** + * Callback method that is called by libev when the timer expires + * @param ec error code returned from loop + * @param loop The loop in which the event was triggered + * @param connection + * @param timeout + */ + void timeout(const boost::system::error_code &ec, + std::weak_ptr awpThis, + TcpConnection *const connection, + const uint16_t timeout) + { + // Resolve any potential problems with dangling pointers + // (remember we are using async). + const std::shared_ptr apTimer = awpThis.lock(); + if (!apTimer) { return; } + + if (!ec) + { + if (connection) + { + // send the heartbeat + connection->heartbeat(); + } + + // Reschedule the timer for the future: + _timer.expires_at(_timer.expires_at() + boost::posix_time::seconds(timeout)); + + // Posts the timer event + _timer.async_wait(get_handler(connection, timeout)); + } + } + + /** + * Stop the timer + */ + void stop() + { + // do nothing if it was never set + _timer.cancel(); + } + + public: + /** + * Constructor + * @param io_service The boost asio io_service. + * @param wpstrand A weak pointer to a io_service::strand instance. + */ + Timer(boost::asio::io_service &io_service, + const strand_weak_ptr wpstrand) : + _ioservice(io_service), + _wpstrand(wpstrand), + _timer(_ioservice) + { + + } + + /** + * Timers cannot be copied or moved + * + * @param that The object to not move or copy + */ + Timer(Timer &&that) = delete; + Timer(const Timer &that) = delete; + + /** + * Destructor + */ + ~Timer() + { + // stop the timer + stop(); + } + + /** + * Change the expire time + * @param connection + * @param timeout + */ + void set(TcpConnection *connection, uint16_t timeout) + { + // stop timer in case it was already set + stop(); + + // Reschedule the timer for the future: + _timer.expires_from_now(boost::posix_time::seconds(timeout)); + + // Posts the timer event + _timer.async_wait(get_handler(connection, timeout)); + } + }; + + /** + * The boost asio io_service. + * @var class boost::asio::io_service& + */ + boost::asio::io_service & _ioservice; + + using strand_shared_ptr = std::shared_ptr; + + /** + * The boost asio io_service::strand managed pointer. + * @var class std::shared_ptr + */ + strand_shared_ptr _strand; + + /** + * All I/O watchers that are active, indexed by their filedescriptor + * @var std::map + */ + std::map > _watchers; + + /** + * The boost asio io_service::deadline_timer managed pointer. + * @var class std::shared_ptr + */ + std::shared_ptr _timer; + + /** + * Method that is called by AMQP-CPP to register a filedescriptor for readability or writability + * @param connection The TCP connection object that is reporting + * @param fd The filedescriptor to be monitored + * @param flags Should the object be monitored for readability or writability? + */ + void monitor(TcpConnection *const connection, + const int fd, + const int flags) override + { + // do we already have this filedescriptor + auto iter = _watchers.find(fd); + + // was it found? + if (iter == _watchers.end()) + { + // we did not yet have this watcher - but that is ok if no filedescriptor was registered + if (flags == 0){ return; } + + // construct a new pair (watcher/timer), and put it in the map + const std::shared_ptr apWatcher = + std::make_shared(_ioservice, _strand, fd); + + _watchers[fd] = apWatcher; + + // explicitly set the events to monitor + apWatcher->events(connection, fd, flags); + } + else if (flags == 0) + { + // the watcher does already exist, but we no longer have to watch this watcher + _watchers.erase(iter); + } + else + { + // Change the events on which to act. + iter->second->events(connection,fd,flags); + } + } + +protected: + /** + * Method that is called when the heartbeat frequency is negotiated between the server and the client. + * @param connection The connection that suggested a heartbeat interval + * @param interval The suggested interval from the server + * @return uint16_t The interval to use + */ + virtual uint16_t onNegotiate(TcpConnection *connection, uint16_t interval) override + { + // skip if no heartbeats are needed + if (interval == 0) return 0; + + // set the timer + _timer->set(connection, interval); + + // we agree with the interval + return interval; + } + +public: + + /** + * Handler cannot be default constructed. + * + * @param that The object to not move or copy + */ + LibBoostAsioHandler() = delete; + + /** + * Constructor + * @param io_service The boost io_service to wrap + */ + explicit LibBoostAsioHandler(boost::asio::io_service &io_service) : + _ioservice(io_service), + _strand(std::make_shared(_ioservice)), + _timer(std::make_shared(_ioservice,_strand)) + { + + } + + /** + * Handler cannot be copied or moved + * + * @param that The object to not move or copy + */ + LibBoostAsioHandler(LibBoostAsioHandler &&that) = delete; + LibBoostAsioHandler(const LibBoostAsioHandler &that) = delete; + + /** + * Returns a reference to the boost io_service object that is being used. + * @return The boost io_service object. + */ + boost::asio::io_service &service() + { + return _ioservice; + } + + /** + * Destructor + */ + ~LibBoostAsioHandler() override = default; + + /** + * Make sure to stop the heartbeat timer after the connection is closed. + * Otherwise it will keep the service running forever. + */ + void onClosed(TcpConnection* connection) override + { + (void)connection; + _timer.reset(); + } +}; + + +/** + * End of namespace + */ +} diff --git a/include/libev.h b/include/amqpcpp/libev.h similarity index 56% rename from include/libev.h rename to include/amqpcpp/libev.h index d489657..1fc1f32 100644 --- a/include/libev.h +++ b/include/amqpcpp/libev.h @@ -8,7 +8,7 @@ * Compile with: "g++ -std=c++11 libev.cpp -lamqpcpp -lev -lpthread" * * @author Emiel Bruijntjes - * @copyright 2015 Copernica BV + * @copyright 2015 - 2018 Copernica BV */ /** @@ -21,6 +21,8 @@ */ #include +#include "amqpcpp/linux_tcp.h" + /** * Set up namespace */ @@ -119,6 +121,111 @@ private: } }; + /** + * Timer class to periodically fire a heartbeat + */ + class Timer + { + private: + /** + * The event loop to which it is attached + * @var struct ev_loop + */ + struct ev_loop *_loop; + + /** + * The actual watcher structure + * @var struct ev_io + */ + struct ev_timer _timer; + + /** + * Callback method that is called by libev when the timer expires + * @param loop The loop in which the event was triggered + * @param timer Internal timer object + * @param revents The events that triggered this call + */ + static void callback(struct ev_loop *loop, struct ev_timer *timer, int revents) + { + // retrieve the connection + TcpConnection *connection = static_cast(timer->data); + + // send the heartbeat + connection->heartbeat(); + } + + /** + * Stop the timer + */ + void stop() + { + // do nothing if it was never set + if (_timer.data == nullptr) return; + + // restore loop refcount + ev_ref(_loop); + + // stop the timer + ev_timer_stop(_loop, &_timer); + + // restore data nullptr to indicate that timer is not set + _timer.data = nullptr; + } + + public: + /** + * Constructor + * @param loop The current event loop + */ + Timer(struct ev_loop *loop) : _loop(loop) + { + // there is no data yet + _timer.data = nullptr; + + // initialize the libev structure + ev_timer_init(&_timer, callback, 60.0, 60.0); + } + + /** + * Watchers cannot be copied or moved + * + * @param that The object to not move or copy + */ + Timer(Watcher &&that) = delete; + Timer(const Watcher &that) = delete; + + /** + * Destructor + */ + virtual ~Timer() + { + // stop the timer + stop(); + } + + /** + * Change the expire time + * @param connection + * @param timeout + */ + void set(TcpConnection *connection, uint16_t timeout) + { + // stop timer in case it was already set + stop(); + + // store the connection in the data "void*" + _timer.data = connection; + + // set the timer + ev_timer_set(&_timer, timeout, timeout); + + // and start it + ev_timer_start(_loop, &_timer); + + // the timer should not keep the event loop active + ev_unref(_loop); + } + }; /** * The event loop @@ -131,6 +238,12 @@ private: * @var std::map */ std::map> _watchers; + + /** + * A timer to periodically send out heartbeats + * @var Timer + */ + Timer _timer; /** @@ -165,12 +278,31 @@ private: } } +protected: + /** + * Method that is called when the heartbeat frequency is negotiated between the server and the client. + * @param connection The connection that suggested a heartbeat interval + * @param interval The suggested interval from the server + * @return uint16_t The interval to use + */ + virtual uint16_t onNegotiate(TcpConnection *connection, uint16_t interval) override + { + // skip if no heartbeats are needed + if (interval == 0) return 0; + + // set the timer + _timer.set(connection, interval); + + // we agree with the interval + return interval; + } + public: /** * Constructor * @param loop The event loop to wrap */ - LibEvHandler(struct ev_loop *loop) : _loop(loop) {} + LibEvHandler(struct ev_loop *loop) : _loop(loop), _timer(loop) {} /** * Destructor diff --git a/include/libevent.h b/include/amqpcpp/libevent.h similarity index 99% rename from include/libevent.h rename to include/amqpcpp/libevent.h index 7621dcc..4d1d19a 100644 --- a/include/libevent.h +++ b/include/amqpcpp/libevent.h @@ -20,6 +20,7 @@ */ #include #include +#include /** * Set up namespace diff --git a/include/libuv.h b/include/amqpcpp/libuv.h similarity index 99% rename from include/libuv.h rename to include/amqpcpp/libuv.h index d48303f..eebf204 100644 --- a/include/libuv.h +++ b/include/amqpcpp/libuv.h @@ -21,6 +21,8 @@ */ #include +#include "amqpcpp/linux_tcp.h" + /** * Set up namespace */ diff --git a/include/amqpcpp/linux_tcp.h b/include/amqpcpp/linux_tcp.h new file mode 100644 index 0000000..c8b0d70 --- /dev/null +++ b/include/amqpcpp/linux_tcp.h @@ -0,0 +1,3 @@ +#include "linux_tcp/tcphandler.h" +#include "linux_tcp/tcpconnection.h" +#include "linux_tcp/tcpchannel.h" diff --git a/include/tcpchannel.h b/include/amqpcpp/linux_tcp/tcpchannel.h similarity index 78% rename from include/tcpchannel.h rename to include/amqpcpp/linux_tcp/tcpchannel.h index 3d19053..e8b5db1 100644 --- a/include/tcpchannel.h +++ b/include/amqpcpp/linux_tcp/tcpchannel.h @@ -4,7 +4,7 @@ * Extended channel that can be constructed on top of a TCP connection * * @author Emiel Bruijntjes - * @copyright 2015 Copernica BV + * @copyright 2015 - 2017 Copernica BV */ /** @@ -25,6 +25,10 @@ class TcpChannel : public Channel public: /** * Constructor + * + * The passed in connection pointer must remain valid for the + * lifetime of the channel. + * * @param connection */ TcpChannel(TcpConnection *connection) : diff --git a/include/tcpconnection.h b/include/amqpcpp/linux_tcp/tcpconnection.h similarity index 86% rename from include/tcpconnection.h rename to include/amqpcpp/linux_tcp/tcpconnection.h index 632063d..15709a0 100644 --- a/include/tcpconnection.h +++ b/include/amqpcpp/linux_tcp/tcpconnection.h @@ -5,7 +5,7 @@ * IO between the client application and the RabbitMQ server. * * @author Emiel Bruijntjes - * @copyright 2015 - 2016 Copernica BV + * @copyright 2015 - 2018 Copernica BV */ /** @@ -34,9 +34,7 @@ private: /** * The state of the TCP connection - this state objecs changes based on * the state of the connection (resolving, connected or closed) - * a shared pointer is used because we use a forward declaration, which isn't - * allowed in a unique pointer - * @var std::shared_ptr + * @var std::unique_ptr */ std::shared_ptr _state; @@ -100,6 +98,7 @@ private: /** * Classes that have access to private data */ + friend class SslConnected; friend class TcpConnected; friend class TcpChannel; @@ -115,7 +114,13 @@ public: /** * Destructor */ - virtual ~TcpConnection() noexcept {} + virtual ~TcpConnection() noexcept; + + /** + * The filedescriptor that is used for this connection + * @return int + */ + int fileno() const; /** * Process the TCP connection @@ -150,7 +155,7 @@ public: } /** - * The max frame size + * The max frame size. Useful if you set up a buffer to parse incoming data: it does not have to exceed this size. * @return uint32_t */ uint32_t maxFrame() const @@ -159,7 +164,7 @@ public: } /** - * The number of bytes that can best be passed to the next call to the parse() method + * The number of bytes that can best be passed to the next call to the parse() method. * @return uint32_t */ uint32_t expected() const @@ -168,7 +173,7 @@ public: } /** - * Return the amount of channels this connection has + * Return the number of channels this connection has. * @return std::size_t */ std::size_t channels() const @@ -176,6 +181,21 @@ public: // return the amount of channels this connection has return _connection.channels(); } + + /** + * The number of outgoing bytes queued on this connection. + * @return std::size_t + */ + std::size_t queued() const; + + /** + * Send a heartbeat + * @return bool + */ + bool heartbeat() + { + return _connection.heartbeat(); + } }; /** diff --git a/include/tcpdefines.h b/include/amqpcpp/linux_tcp/tcpdefines.h similarity index 100% rename from include/tcpdefines.h rename to include/amqpcpp/linux_tcp/tcpdefines.h diff --git a/include/tcphandler.h b/include/amqpcpp/linux_tcp/tcphandler.h similarity index 60% rename from include/tcphandler.h rename to include/amqpcpp/linux_tcp/tcphandler.h index 7c86a4a..9b0c87d 100644 --- a/include/tcphandler.h +++ b/include/amqpcpp/linux_tcp/tcphandler.h @@ -6,7 +6,7 @@ * class. * * @author Emiel Bruijntjes - * @copyright 2015 Copernica BV + * @copyright 2015 - 2018 Copernica BV */ /** @@ -14,6 +14,11 @@ */ #pragma once +/** + * Dependencies + */ +#include + /** * Set up namespace */ @@ -33,11 +38,35 @@ public: /** * Destructor */ - virtual ~TcpHandler() {} + virtual ~TcpHandler() = default; + + /** + * Method that is called after a TCP connection has been set up and the initial + * TLS handshake is finished too, but right before the AMQP login handshake is + * going to take place and the first data is going to be sent over the connection. + * This method allows you to inspect the TLS certificate and other connection + * properties, and to break up the connection if you find it not secure enough. + * The default implementation considers all connections to be secure, even if the + * connection has a self-signed or even invalid certificate. To be more strict, + * override this method, inspect the certificate and return false if you do not + * want to use the connection. The passed in SSL pointer is a pointer to a SSL + * structure from the openssl library. This method is only called for secure + * connections (connection with an amqps:// address). + * @param connection The connection for which TLS was just started + * @param ssl Pointer to the SSL structure that can be inspected + * @return bool True to proceed / accept the connection, false to break up + */ + virtual bool onSecured(TcpConnection *connection, const SSL *ssl) + { + // default implementation: do not inspect anything, just allow the connection + return true; + } /** * Method that is called when the heartbeat frequency is negotiated - * between the server and the client. + * between the server and the client. Applications can override this method + * if they want to use a different heartbeat interval (for example: return 0 + * to disable heartbeats) * @param connection The connection that suggested a heartbeat interval * @param interval The suggested interval from the server * @return uint16_t The interval to use @@ -51,7 +80,9 @@ public: } /** - * Method that is called when the TCP connection ends up in a connected state + * Method that is called when the AMQP connection ends up in a connected state + * This method is called after the TCP connection has been set up, the (optional) + * secure TLS connection, and the AMQP login handshake has been completed. * @param connection The TCP connection */ virtual void onConnected(TcpConnection *connection) {} @@ -96,8 +127,6 @@ public: * @param flags Should the object be monitored for readability or writability? */ virtual void monitor(TcpConnection *connection, int fd, int flags) = 0; - - }; /** diff --git a/include/login.h b/include/amqpcpp/login.h similarity index 65% rename from include/login.h rename to include/amqpcpp/login.h index f68f1aa..f805e18 100644 --- a/include/login.h +++ b/include/amqpcpp/login.h @@ -3,7 +3,7 @@ * * This class combines login, password and vhost * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -65,7 +65,25 @@ public: /** * Destructor */ - virtual ~Login() {} + virtual ~Login() = default; + + /** + * Cast to boolean: is the login set? + * @return bool + */ + operator bool () const + { + return !_user.empty() || !_password.empty(); + } + + /** + * Negate operator: is it not set + * @return bool + */ + bool operator! () const + { + return _user.empty() && _password.empty(); + } /** * Retrieve the user name @@ -119,6 +137,32 @@ public: // the opposite of operator== return !operator==(that); } + + /** + * Comparison operator + * @param that + * @return bool + */ + bool operator<(const Login &that) const + { + // compare users + if (_user != that._user) return _user < that._user; + + // compare passwords + return _password < that._password; + } + + /** + * Friend function to allow writing the login to a stream + * @param stream + * @param login + * @return std::ostream + */ + friend std::ostream &operator<<(std::ostream &stream, const Login &login) + { + // write username and password + return stream << login._user << ":" << login._password; + } }; diff --git a/include/message.h b/include/amqpcpp/message.h similarity index 89% rename from include/message.h rename to include/amqpcpp/message.h index 5ce4e68..7c8f59e 100644 --- a/include/message.h +++ b/include/amqpcpp/message.h @@ -7,7 +7,7 @@ * Message objects can not be constructed by end users, they are only constructed * by the AMQP library, and passed to user callbacks. * - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -21,6 +21,7 @@ #include "envelope.h" #include #include +#include /** * Set up namespace @@ -30,7 +31,7 @@ namespace AMQP { /** * Forward declarations */ -class DeferredConsumerBase; +class DeferredReceiver; /** * Class definition @@ -60,7 +61,7 @@ protected: /** * We are an open book to the consumer handler */ - friend class DeferredConsumerBase; + friend class DeferredReceiver; /** * Set the body size @@ -93,10 +94,10 @@ protected: size = std::min(size, _bodySize - _filled); // append more data - memcpy(_body + _filled, buffer, size); + memcpy(_body + _filled, buffer, (size_t)size); // update filled data - _filled += size; + _filled += (size_t)size; } else if (size >= _bodySize) { @@ -107,16 +108,16 @@ protected: else { // allocate the buffer - _body = (char *)malloc(_bodySize); + _body = (char *)malloc((size_t)_bodySize); // remember that the buffer was allocated, so that the destructor can get rid of it _allocated = true; // append more data - memcpy(_body, buffer, std::min(size, _bodySize)); + memcpy(_body, buffer, std::min((size_t)size, (size_t)_bodySize)); // update filled data - _filled = std::min(size, _bodySize); + _filled = std::min((size_t)size, (size_t)_bodySize); } // check if we're done diff --git a/include/metadata.h b/include/amqpcpp/metadata.h similarity index 91% rename from include/metadata.h rename to include/amqpcpp/metadata.h index 4b1a04e..8b18d99 100644 --- a/include/metadata.h +++ b/include/amqpcpp/metadata.h @@ -302,20 +302,20 @@ public: // the result (2 for the two boolean sets) uint32_t result = 2; - if (hasExpiration()) result += _expiration.size(); - if (hasReplyTo()) result += _replyTo.size(); - if (hasCorrelationID()) result += _correlationID.size(); - if (hasPriority()) result += _priority.size(); - if (hasDeliveryMode()) result += _deliveryMode.size(); - if (hasHeaders()) result += _headers.size(); - if (hasContentEncoding()) result += _contentEncoding.size(); - if (hasContentType()) result += _contentType.size(); - if (hasClusterID()) result += _clusterID.size(); - if (hasAppID()) result += _appID.size(); - if (hasUserID()) result += _userID.size(); - if (hasTypeName()) result += _typeName.size(); - if (hasTimestamp()) result += _timestamp.size(); - if (hasMessageID()) result += _messageID.size(); + if (hasExpiration()) result += (uint32_t)_expiration.size(); + if (hasReplyTo()) result += (uint32_t)_replyTo.size(); + if (hasCorrelationID()) result += (uint32_t)_correlationID.size(); + if (hasPriority()) result += (uint32_t)_priority.size(); + if (hasDeliveryMode()) result += (uint32_t)_deliveryMode.size(); + if (hasHeaders()) result += (uint32_t)_headers.size(); + if (hasContentEncoding()) result += (uint32_t)_contentEncoding.size(); + if (hasContentType()) result += (uint32_t)_contentType.size(); + if (hasClusterID()) result += (uint32_t)_clusterID.size(); + if (hasAppID()) result += (uint32_t)_appID.size(); + if (hasUserID()) result += (uint32_t)_userID.size(); + if (hasTypeName()) result += (uint32_t)_typeName.size(); + if (hasTimestamp()) result += (uint32_t)_timestamp.size(); + if (hasMessageID()) result += (uint32_t)_messageID.size(); // done return result; diff --git a/include/monitor.h b/include/amqpcpp/monitor.h similarity index 81% rename from include/monitor.h rename to include/amqpcpp/monitor.h index 59564f9..efb788c 100644 --- a/include/monitor.h +++ b/include/amqpcpp/monitor.h @@ -8,7 +8,7 @@ * case the connection object should stop further handling the data. This * monitor class is used to check if the connection has been destructed. * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -76,6 +76,24 @@ public: if (_watchable) _watchable->remove(this); } + /** + * Cast to boolean: is object in valid state? + * @return bool + */ + operator bool () const + { + return _watchable != nullptr; + } + + /** + * Negate operator: is the object in an invalid state? + * @return bool + */ + bool operator! () const + { + return _watchable == nullptr; + } + /** * Check if the object is valid * @return bool diff --git a/include/numericfield.h b/include/amqpcpp/numericfield.h similarity index 89% rename from include/numericfield.h rename to include/amqpcpp/numericfield.h index 29f2f42..a54c3f8 100644 --- a/include/numericfield.h +++ b/include/amqpcpp/numericfield.h @@ -1,6 +1,6 @@ /** * Numeric field types for AMQP - * + * * @copyright 2014 Copernica BV */ @@ -42,6 +42,8 @@ private: T _value; public: + using Type = T; + /** * Default constructor, assign 0 */ @@ -116,14 +118,14 @@ public: * Get the value * @return mixed */ - operator uint8_t () const override { return _value; } - operator uint16_t() const override { return _value; } - operator uint32_t() const override { return _value; } - operator uint64_t() const override { return _value; } - operator int8_t () const override { return _value; } - operator int16_t () const override { return _value; } - operator int32_t () const override { return _value; } - operator int64_t () const override { return _value; } + operator uint8_t () const override { return (uint8_t)_value; } + operator uint16_t() const override { return (uint16_t)_value; } + operator uint32_t() const override { return (uint32_t)_value; } + operator uint64_t() const override { return (uint64_t)_value; } + operator int8_t () const override { return (int8_t)_value; } + operator int16_t () const override { return (int16_t)_value; } + operator int32_t () const override { return (int32_t)_value; } + operator int64_t () const override { return (int64_t)_value; } /** * Get the value @@ -213,4 +215,3 @@ typedef NumericField Double; * end namespace */ } - diff --git a/include/amqpcpp/openssl.h b/include/amqpcpp/openssl.h new file mode 100644 index 0000000..f4ac1e1 --- /dev/null +++ b/include/amqpcpp/openssl.h @@ -0,0 +1,37 @@ +/** + * OpenSSL.h + * + * Function to set openssl features + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * To make secure "amqps://" connections, AMQP-CPP relies on functions from the + * openssl library. It your application is dynamically linked to openssl (because + * it was compiled with the "-lssl" flag), this works flawlessly because AMQPCPP + * can then locate the openssl symbols in its own project space. However, if the + * openssl library was not linked, you either cannot use amqps:// connections, + * or you have to supply a handle to the openssl library yourself, using the + * following method. + * + * @param handle Handle returned by dlopen() that has access to openssl + */ +void openssl(void *handle); + +/** + * End of namespace + */ +} + diff --git a/include/outbuffer.h b/include/amqpcpp/outbuffer.h similarity index 100% rename from include/outbuffer.h rename to include/amqpcpp/outbuffer.h diff --git a/include/protocolexception.h b/include/amqpcpp/protocolexception.h similarity index 100% rename from include/protocolexception.h rename to include/amqpcpp/protocolexception.h diff --git a/include/receivedframe.h b/include/amqpcpp/receivedframe.h similarity index 100% rename from include/receivedframe.h rename to include/amqpcpp/receivedframe.h diff --git a/include/stack_ptr.h b/include/amqpcpp/stack_ptr.h similarity index 100% rename from include/stack_ptr.h rename to include/amqpcpp/stack_ptr.h diff --git a/include/stringfield.h b/include/amqpcpp/stringfield.h similarity index 97% rename from include/stringfield.h rename to include/amqpcpp/stringfield.h index bab6310..edcee29 100644 --- a/include/stringfield.h +++ b/include/amqpcpp/stringfield.h @@ -119,7 +119,7 @@ public: virtual size_t size() const override { // find out size of the size parameter - T size(_data.size()); + T size((typename T::Type)_data.size()); // size of the uint8 or uint32 + the actual string size return size.size() + _data.size(); @@ -160,7 +160,7 @@ public: virtual void fill(OutBuffer& buffer) const override { // create size - T size(_data.size()); + T size((typename T::Type)_data.size()); // first, write down the size of the string size.fill(buffer); @@ -210,4 +210,3 @@ typedef StringField LongString; * end namespace */ } - diff --git a/include/table.h b/include/amqpcpp/table.h similarity index 100% rename from include/table.h rename to include/amqpcpp/table.h diff --git a/include/watchable.h b/include/amqpcpp/watchable.h similarity index 100% rename from include/watchable.h rename to include/amqpcpp/watchable.h diff --git a/include/callbacks.h b/include/callbacks.h deleted file mode 100644 index 8037a88..0000000 --- a/include/callbacks.h +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Callbacks.h - * - * Class storing deferred callbacks of different type. - * - * @copyright 2014 - 2017 Copernica BV - */ - -/** - * Include guard - */ -#pragma once - -/** - * Dependencies - */ -#include -#include - -/** - * Set up namespace - */ -namespace AMQP { - -/** - * Forward declarations - */ -class Message; -class MetaData; - -/** - * All the callbacks that are supported - * - * When someone registers a callback function for certain events, it should - * match one of the following signatures. - */ -using SuccessCallback = std::function; -using ErrorCallback = std::function; -using FinalizeCallback = std::function; -using EmptyCallback = std::function; -using BeginCallback = std::function; -using HeaderCallback = std::function; -using DataCallback = std::function; -using MessageCallback = std::function; -using CompleteCallback = std::function; -using QueueCallback = std::function; -using DeleteCallback = std::function; -using SizeCallback = std::function; -using ConsumeCallback = std::function; -using CancelCallback = std::function; -using AckCallback = std::function; -using NackCallback = std::function; - -/** - * End namespace - */ -} diff --git a/set_cxx_norm.cmake b/set_cxx_norm.cmake deleted file mode 100644 index da2af82..0000000 --- a/set_cxx_norm.cmake +++ /dev/null @@ -1,55 +0,0 @@ -# Version -cmake_minimum_required(VERSION 2.6.3) - -set(CXX_NORM_CXX98 1) # C++98 -set(CXX_NORM_CXX03 2) # C++03 -set(CXX_NORM_CXX11 3) # C++11 - -# - Set the wanted C++ norm -# Adds the good argument to the command line in function of the compiler -# Currently only works with g++ and clang++ -macro(set_cxx_norm NORM) - - # Extract c++ compiler --version output - exec_program( - ${CMAKE_CXX_COMPILER} - ARGS --version - OUTPUT_VARIABLE _compiler_output - ) - # Keep only the first line - string(REGEX REPLACE - "(\n.*$)" - "" - cxx_compiler_version "${_compiler_output}" - ) - # Extract the version number - string(REGEX REPLACE - "([^0-9.])|([0-9.][^0-9.])" - "" - cxx_compiler_version "${cxx_compiler_version}" - ) - - if(CMAKE_COMPILER_IS_GNUCXX) - - if(${NORM} EQUAL ${CXX_NORM_CXX98}) - add_definitions("-std=c++98") - elseif(${NORM} EQUAL ${CXX_NORM_CXX03}) - add_definitions("-std=c++03") - elseif(${NORM} EQUAL ${CXX_NORM_CXX11}) - if(${cxx_compiler_version} VERSION_LESS "4.7.0") - add_definitions("-std=c++0x") - else() - add_definitions("-std=c++11") - endif() - endif() - - elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") - - if(${NORM} EQUAL ${CXX_NORM_CXX11}) - add_definitions("-std=c++11") - endif() - - endif() - -endmacro() - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e06b204..b900c46 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,102 +1,95 @@ add_sources( -addressinfo.h -array.cpp -basicackframe.h -basiccancelframe.h -basiccancelokframe.h -basicconsumeframe.h -basicconsumeokframe.h -basicdeliverframe.h -basicframe.h -basicgetemptyframe.h -basicgetframe.h -basicgetokframe.h -basicheaderframe.h -basicnackframe.h -basicpublishframe.h -basicqosframe.h -basicqosokframe.h -basicrecoverasyncframe.h -basicrecoverframe.h -basicrecoverokframe.h -basicrejectframe.h -basicreturnframe.h -bodyframe.h -channelcloseframe.h -channelcloseokframe.h -channelflowframe.h -channelflowokframe.h -channelframe.h -channelimpl.cpp -channelopenframe.h -channelopenokframe.h -confirmselectframe.h -confirmselectokframe.h -connectioncloseframe.h -connectioncloseokframe.h -connectionframe.h -connectionimpl.cpp -connectionopenframe.h -connectionopenokframe.h -connectionsecureframe.h -connectionsecureokframe.h -connectionstartframe.h -connectionstartokframe.h -connectiontuneframe.h -connectiontuneokframe.h -consumedmessage.h -deferredcancel.cpp -deferredconsumer.cpp -deferredconsumerbase.cpp -deferredget.cpp -exchangebindframe.h -exchangebindokframe.h -exchangedeclareframe.h -exchangedeclareokframe.h -exchangedeleteframe.h -exchangedeleteokframe.h -exchangeframe.h -exchangeunbindframe.h -exchangeunbindokframe.h -extframe.h -field.cpp -flags.cpp -framecheck.h -headerframe.h -heartbeatframe.h -includes.h -methodframe.h -passthroughbuffer.h -pipe.h -protocolheaderframe.h -queuebindframe.h -queuebindokframe.h -queuedeclareframe.h -queuedeclareokframe.h -queuedeleteframe.h -queuedeleteokframe.h -queueframe.h -queuepurgeframe.h -queuepurgeokframe.h -queueunbindframe.h -queueunbindokframe.h -receivedframe.cpp -reducedbuffer.h -returnedmessage.h -table.cpp -tcpclosed.h -tcpconnected.h -tcpconnection.cpp -tcpinbuffer.h -tcpoutbuffer.h -tcpresolver.h -tcpstate.h -transactioncommitframe.h -transactioncommitokframe.h -transactionframe.h -transactionrollbackframe.h -transactionrollbackokframe.h -transactionselectframe.h -transactionselectokframe.h -watchable.cpp + array.cpp + basicackframe.h + basiccancelframe.h + basiccancelokframe.h + basicconsumeframe.h + basicconsumeokframe.h + basicdeliverframe.h + basicframe.h + basicgetemptyframe.h + basicgetframe.h + basicgetokframe.h + basicheaderframe.h + basicnackframe.h + basicpublishframe.h + basicqosframe.h + basicqosokframe.h + basicrecoverasyncframe.h + basicrecoverframe.h + basicrecoverokframe.h + basicrejectframe.h + basicreturnframe.h + bodyframe.h + channelcloseframe.h + channelcloseokframe.h + channelflowframe.h + channelflowokframe.h + channelframe.h + channelimpl.cpp + channelopenframe.h + channelopenokframe.h + confirmselectframe.h + confirmselectokframe.h + connectioncloseframe.h + connectioncloseokframe.h + connectionframe.h + connectionimpl.cpp + connectionopenframe.h + connectionopenokframe.h + connectionsecureframe.h + connectionsecureokframe.h + connectionstartframe.h + connectionstartokframe.h + connectiontuneframe.h + connectiontuneokframe.h + consumedmessage.h + deferredcancel.cpp + deferredconsumer.cpp + deferredreceiver.cpp + deferredextreceiver.cpp + deferredpublisher.cpp + deferredget.cpp + exchangebindframe.h + exchangebindokframe.h + exchangedeclareframe.h + exchangedeclareokframe.h + exchangedeleteframe.h + exchangedeleteokframe.h + exchangeframe.h + exchangeunbindframe.h + exchangeunbindokframe.h + extframe.h + field.cpp + flags.cpp + framecheck.h + headerframe.h + heartbeatframe.h + includes.h + methodframe.h + passthroughbuffer.h + protocolheaderframe.h + queuebindframe.h + queuebindokframe.h + queuedeclareframe.h + queuedeclareokframe.h + queuedeleteframe.h + queuedeleteokframe.h + queueframe.h + queuepurgeframe.h + queuepurgeokframe.h + queueunbindframe.h + queueunbindokframe.h + receivedframe.cpp + reducedbuffer.h + returnedmessage.h + table.cpp + transactioncommitframe.h + transactioncommitokframe.h + transactionframe.h + transactionrollbackframe.h + transactionrollbackokframe.h + transactionselectframe.h + transactionselectokframe.h + watchable.cpp ) diff --git a/src/Makefile b/src/Makefile index 7448733..3471fd5 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,11 +1,11 @@ CPP = g++ RM = rm -f -CPPFLAGS = -Wall -c -I. -std=c++11 -MD +CPPFLAGS = -Wall -c -I../include -std=c++11 -MD LD = g++ LD_FLAGS = -Wall -shared SHARED_LIB = lib$(LIBRARY_NAME).so.$(VERSION) STATIC_LIB = lib$(LIBRARY_NAME).a.$(VERSION) -SOURCES = $(wildcard *.cpp) +SOURCES = $(wildcard *.cpp) $(wildcard linux_tcp/*.cpp) SHARED_OBJECTS = $(SOURCES:%.cpp=%.o) STATIC_OBJECTS = $(SOURCES:%.cpp=%.s.o) DEPENDENCIES = $(SOURCES:%.cpp=%.d) @@ -53,4 +53,3 @@ ${SHARED_OBJECTS}: ${STATIC_OBJECTS}: ${CPP} ${CPPFLAGS} -o $@ ${@:%.s.o=%.cpp} - diff --git a/src/array.cpp b/src/array.cpp index d04c865..9017faf 100644 --- a/src/array.cpp +++ b/src/array.cpp @@ -29,7 +29,7 @@ Array::Array(ReceivedFrame &frame) if (!field) continue; // less bytes to read - charsToRead -= field->size(); + charsToRead -= (uint32_t)field->size(); // add the additional field _fields.push_back(std::shared_ptr(field)); @@ -76,7 +76,7 @@ const Field &Array::get(uint8_t index) const */ uint32_t Array::count() const { - return _fields.size(); + return (uint32_t)_fields.size(); } /** diff --git a/src/basiccancelframe.h b/src/basiccancelframe.h index 6dd26dd..cdcf916 100644 --- a/src/basiccancelframe.h +++ b/src/basiccancelframe.h @@ -59,7 +59,7 @@ public: * @param noWait whether to wait for a response. */ BasicCancelFrame(uint16_t channel, const std::string& consumerTag, bool noWait = false) : - BasicFrame(channel, consumerTag.size() + 2), // 1 for extra string size, 1 for bool + BasicFrame(channel, (uint32_t)(consumerTag.size() + 2)), // 1 for extra string size, 1 for bool _consumerTag(consumerTag), _noWait(noWait) {} diff --git a/src/basiccancelokframe.h b/src/basiccancelokframe.h index 192417f..a894c5c 100644 --- a/src/basiccancelokframe.h +++ b/src/basiccancelokframe.h @@ -53,7 +53,7 @@ public: * @param consumerTag holds the consumertag specified by client or server */ BasicCancelOKFrame(uint16_t channel, std::string& consumerTag) : - BasicFrame(channel, consumerTag.length() + 1), // add 1 byte for encoding the size of consumer tag + BasicFrame(channel, (uint32_t)(consumerTag.length() + 1)), // add 1 byte for encoding the size of consumer tag _consumerTag(consumerTag) {} diff --git a/src/basicconsumeframe.h b/src/basicconsumeframe.h index 7e57eee..39a8930 100644 --- a/src/basicconsumeframe.h +++ b/src/basicconsumeframe.h @@ -86,7 +86,7 @@ public: * @param filter additional arguments */ BasicConsumeFrame(uint16_t channel, const std::string& queueName, const std::string& consumerTag, bool noLocal = false, bool noAck = false, bool exclusive = false, bool noWait = false, const Table& filter = {}) : - BasicFrame(channel, (queueName.length() + consumerTag.length() + 5 + filter.size())), // size of vars, +1 for each shortstring size, +1 for bools, +2 for deprecated value + BasicFrame(channel, (uint32_t)(queueName.length() + consumerTag.length() + 5 + filter.size())), // size of vars, +1 for each shortstring size, +1 for bools, +2 for deprecated value _queueName(queueName), _consumerTag(consumerTag), _bools(noLocal, noAck, exclusive, noWait), diff --git a/src/basicconsumeokframe.h b/src/basicconsumeokframe.h index 927fa38..9ec6397 100644 --- a/src/basicconsumeokframe.h +++ b/src/basicconsumeokframe.h @@ -43,7 +43,7 @@ public: * @param consumerTag consumertag specified by client of provided by server */ BasicConsumeOKFrame(uint16_t channel, const std::string& consumerTag) : - BasicFrame(channel, consumerTag.length() + 1), // length of string + 1 for encoding of stringsize + BasicFrame(channel, (uint32_t)(consumerTag.length() + 1)), // length of string + 1 for encoding of stringsize _consumerTag(consumerTag) {} diff --git a/src/basicdeliverframe.h b/src/basicdeliverframe.h index 1252969..07dad11 100644 --- a/src/basicdeliverframe.h +++ b/src/basicdeliverframe.h @@ -1,7 +1,7 @@ /** * Class describing a basic deliver frame * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -13,9 +13,10 @@ * Dependencies */ #include "basicframe.h" -#include "../include/stringfield.h" -#include "../include/booleanset.h" -#include "../include/connectionimpl.h" +#include "amqpcpp/stringfield.h" +#include "amqpcpp/booleanset.h" +#include "amqpcpp/connectionimpl.h" +#include "amqpcpp/deferredconsumer.h" /** * Set up namespace @@ -87,7 +88,7 @@ public: * @param routingKey message routing key */ BasicDeliverFrame(uint16_t channel, const std::string& consumerTag, uint64_t deliveryTag, bool redelivered = false, const std::string& exchange = "", const std::string& routingKey = "") : - BasicFrame(channel, (consumerTag.length() + exchange.length() + routingKey.length() + 12)), + BasicFrame(channel, (uint32_t)(consumerTag.length() + exchange.length() + routingKey.length() + 12)), // length of strings + 1 byte per string for stringsize, 8 bytes for uint64_t and 1 for bools _consumerTag(consumerTag), _deliveryTag(deliveryTag), @@ -193,8 +194,14 @@ public: // channel does not exist if (!channel) return false; - // construct the message - channel->process(*this); + // get the appropriate consumer object + auto consumer = channel->consumer(_consumerTag); + + // skip if there was no consumer for this tag + if (consumer == nullptr) return false; + + // initialize the object, because we're about to receive a message + consumer->process(*this); // done return true; diff --git a/src/basicgetframe.h b/src/basicgetframe.h index f7244c8..4d112ff 100644 --- a/src/basicgetframe.h +++ b/src/basicgetframe.h @@ -59,7 +59,7 @@ public: * @param noAck whether server expects acknowledgements for messages */ BasicGetFrame(uint16_t channel, const std::string& queue, bool noAck = false) : - BasicFrame(channel, queue.length() + 4), // 1 for bool, 1 for string size, 2 for deprecated field + BasicFrame(channel, (uint32_t)(queue.length() + 4)), // 1 for bool, 1 for string size, 2 for deprecated field _queue(queue), _noAck(noAck) {} diff --git a/src/basicgetokframe.h b/src/basicgetokframe.h index 5cc3a12..97bc7af 100644 --- a/src/basicgetokframe.h +++ b/src/basicgetokframe.h @@ -1,7 +1,7 @@ /** * Class describing a basic get ok frame * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -76,7 +76,7 @@ public: * @param messageCount number of messages in the queue */ BasicGetOKFrame(uint16_t channel, uint64_t deliveryTag, bool redelivered, const std::string& exchange, const std::string& routingKey, uint32_t messageCount) : - BasicFrame(channel, (exchange.length() + routingKey.length() + 15)), // string length, +1 for each shortsrting length + 8 (uint64_t) + 4 (uint32_t) + 1 (bool) + BasicFrame(channel, (uint32_t)(exchange.length() + routingKey.length() + 15)), // string length, +1 for each shortsrting length + 8 (uint64_t) + 4 (uint32_t) + 1 (bool) _deliveryTag(deliveryTag), _redelivered(redelivered), _exchange(exchange), @@ -170,14 +170,17 @@ public: // channel does not exist if (!channel) return false; - // report success for the get operation - channel->reportSuccess(messageCount(), deliveryTag(), redelivered()); + // report success for the get operation (this will also update the current receiver!) + channel->reportSuccess(messageCount(), _deliveryTag, redelivered()); - // check if we have a valid consumer - if (!channel->consumer()) return false; + // get the current receiver object + auto *receiver = channel->receiver(); - // pass on to consumer - channel->consumer()->process(*this); + // check if we have a valid receiver + if (receiver == nullptr) return false; + + // initialize the receiver for the upcoming message + receiver->initialize(_exchange, _routingKey); // done return true; diff --git a/src/basicheaderframe.h b/src/basicheaderframe.h index 5260621..bc60a17 100644 --- a/src/basicheaderframe.h +++ b/src/basicheaderframe.h @@ -1,7 +1,7 @@ /** * Class describing an AMQP basic header frame * - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -13,10 +13,10 @@ * Dependencies */ #include "headerframe.h" -#include "../include/metadata.h" -#include "../include/envelope.h" -#include "../include/connectionimpl.h" -#include "../include/deferredconsumerbase.h" +#include "amqpcpp/metadata.h" +#include "amqpcpp/envelope.h" +#include "amqpcpp/connectionimpl.h" +#include "amqpcpp/deferredreceiver.h" /** * Set up namespace @@ -134,12 +134,18 @@ public: { // we need the appropriate channel auto channel = connection->channel(this->channel()); + + // we need a channel + if (channel == nullptr) return false; + + // do we have an object that is receiving this data? + auto *receiver = channel->receiver(); // check if we have a valid channel and consumer - if (!channel || !channel->consumer()) return false; + if (receiver == nullptr) return false; // the channel can process the frame - channel->consumer()->process(*this); + receiver->process(*this); // done return true; diff --git a/src/basicpublishframe.h b/src/basicpublishframe.h index 975c2ab..bbaed57 100644 --- a/src/basicpublishframe.h +++ b/src/basicpublishframe.h @@ -70,7 +70,7 @@ public: * @param immediate request immediate delivery @default = false */ BasicPublishFrame(uint16_t channel, const std::string& exchange = "", const std::string& routingKey = "", bool mandatory = false, bool immediate = false) : - BasicFrame(channel, exchange.length() + routingKey.length() + 5), // 1 extra per string (for the size), 1 for bools, 2 for deprecated field + BasicFrame(channel, (uint32_t)(exchange.length() + routingKey.length() + 5)), // 1 extra per string (for the size), 1 for bools, 2 for deprecated field _exchange(exchange), _routingKey(routingKey), _bools(mandatory, immediate) diff --git a/src/basicreturnframe.h b/src/basicreturnframe.h index 2fedce6..f14ff65 100644 --- a/src/basicreturnframe.h +++ b/src/basicreturnframe.h @@ -1,7 +1,7 @@ /** * Class describing a basic return frame * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -67,7 +67,7 @@ public: * @param routingKey message routing key */ BasicReturnFrame(uint16_t channel, int16_t replyCode, const std::string& replyText = "", const std::string& exchange = "", const std::string& routingKey = "") : - BasicFrame(channel, replyText.length() + exchange.length() + routingKey.length() + 5), // 3 for each string (extra size byte), 2 for uint16_t + BasicFrame(channel, (uint32_t)(replyText.length() + exchange.length() + routingKey.length() + 5)), // 3 for each string (extra size byte), 2 for uint16_t _replyCode(replyCode), _replyText(replyText), _exchange(exchange), @@ -155,8 +155,23 @@ public: */ virtual bool process(ConnectionImpl *connection) override { - // we no longer support returned messages - return false; + // we need the appropriate channel + auto channel = connection->channel(this->channel()); + + // channel does not exist + if (!channel) return false; + + // get the current publisher + auto publisher = channel->publisher(); + + // if there is no deferred publisher, we can just as well stop + if (publisher == nullptr) return false; + + // initialize the object, because we're about to receive a message + publisher->process(*this); + + // done + return true; } }; diff --git a/src/bodyframe.h b/src/bodyframe.h index ba34cd2..5dc4920 100644 --- a/src/bodyframe.h +++ b/src/bodyframe.h @@ -1,7 +1,7 @@ /** * Class describing an AMQP Body Frame * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -13,8 +13,8 @@ * Dependencies */ #include "extframe.h" -#include "../include/connectionimpl.h" -#include "../include/deferredconsumerbase.h" +#include "amqpcpp/connectionimpl.h" +#include "amqpcpp/deferredreceiver.h" /** * Set up namespace @@ -105,12 +105,18 @@ public: { // we need the appropriate channel auto channel = connection->channel(this->channel()); + + // we must have a channel object + if (channel == nullptr) return false; + + // get the object that is receiving the messages + auto *receiver = channel->receiver(); - // check if we have a valid channel and consumer - if (!channel || !channel->consumer()) return false; + // check if we have a valid receiver + if (receiver == nullptr) return false; // the consumer may process the frame - channel->consumer()->process(*this); + receiver->process(*this); // done return true; diff --git a/src/channelcloseframe.h b/src/channelcloseframe.h index d47d88d..48476af 100644 --- a/src/channelcloseframe.h +++ b/src/channelcloseframe.h @@ -82,7 +82,7 @@ public: * @param failingMethod failing method id if applicable */ ChannelCloseFrame(uint16_t channel, uint16_t code = 0, const std::string& text = "", uint16_t failingClass = 0, uint16_t failingMethod = 0) : - ChannelFrame(channel, (text.length() + 7)), // sizeof code, failingclass, failingmethod (2byte + 2byte + 2byte) + text length + text length byte + ChannelFrame(channel, (uint32_t)(text.length() + 7)), // sizeof code, failingclass, failingmethod (2byte + 2byte + 2byte) + text length + text length byte _code(code), _text(text), _failingClass(failingClass), diff --git a/src/channelimpl.cpp b/src/channelimpl.cpp index 11ec906..21d191c 100644 --- a/src/channelimpl.cpp +++ b/src/channelimpl.cpp @@ -3,10 +3,9 @@ * * Implementation for a channel * - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ #include "includes.h" -#include "basicdeliverframe.h" #include "basicgetokframe.h" #include "basicreturnframe.h" #include "consumedmessage.h" @@ -40,7 +39,6 @@ #include "basicrejectframe.h" #include "basicgetframe.h" - /** * Set up namespace */ @@ -266,7 +264,7 @@ Deferred &ChannelImpl::close() Deferred &ChannelImpl::declareExchange(const std::string &name, ExchangeType type, int flags, const Table &arguments) { // convert exchange type - std::string exchangeType; + const char *exchangeType = ""; // convert the exchange type into a string if (type == ExchangeType::fanout) exchangeType = "fanout"; @@ -275,8 +273,15 @@ Deferred &ChannelImpl::declareExchange(const std::string &name, ExchangeType typ else if (type == ExchangeType::headers) exchangeType = "headers"; else if (type == ExchangeType::consistent_hash) exchangeType = "x-consistent-hash"; + // the boolean options + bool passive = flags & AMQP::passive; + bool durable = flags & AMQP::durable; + bool autodelete = flags & AMQP::autodelete; + bool internal = flags & AMQP::internal; + bool nowait = flags & AMQP::nowait; + // send declare exchange frame - return push(ExchangeDeclareFrame(_id, name, exchangeType, (flags & passive) != 0, (flags & durable) != 0, false, arguments)); + return push(ExchangeDeclareFrame(_id, name, exchangeType, passive, durable, autodelete, internal, nowait, arguments)); } /** @@ -459,26 +464,31 @@ DeferredDelete &ChannelImpl::removeQueue(const std::string &name, int flags) * @param envelope the full envelope to send * @param message the message to send * @param size size of the message + * @param flags + * @return DeferredPublisher */ -bool ChannelImpl::publish(const std::string &exchange, const std::string &routingKey, const Envelope &envelope) +DeferredPublisher &ChannelImpl::publish(const std::string &exchange, const std::string &routingKey, const Envelope &envelope, int flags) { // we are going to send out multiple frames, each one will trigger a call to the handler, // which in turn could destruct the channel object, we need to monitor that Monitor monitor(this); // @todo do not copy the entire buffer to individual frames + + // make sure we have a deferred object to return + if (!_publisher) _publisher.reset(new DeferredPublisher(this)); // send the publish frame - if (!send(BasicPublishFrame(_id, exchange, routingKey))) return false; + if (!send(BasicPublishFrame(_id, exchange, routingKey, (flags & mandatory) != 0, (flags & immediate) != 0))) return *_publisher; // channel still valid? - if (!monitor.valid()) return false; + if (!monitor.valid()) return *_publisher; // send header - if (!send(BasicHeaderFrame(_id, envelope))) return false; + if (!send(BasicHeaderFrame(_id, envelope))) return *_publisher; // channel and connection still valid? - if (!monitor.valid() || !_connection) return false; + if (!monitor.valid() || !_connection) return *_publisher; // the max payload size is the max frame size minus the bytes for headers and trailer uint32_t maxpayload = _connection->maxPayload(); @@ -495,10 +505,10 @@ bool ChannelImpl::publish(const std::string &exchange, const std::string &routin uint64_t chunksize = std::min(static_cast(maxpayload), bytesleft); // send out a body frame - if (!send(BodyFrame(_id, data + bytessent, chunksize))) return false; + if (!send(BodyFrame(_id, data + bytessent, (uint32_t)chunksize))) return *_publisher; // channel still valid? - if (!monitor.valid()) return false; + if (!monitor.valid()) return *_publisher; // update counters bytessent += chunksize; @@ -509,7 +519,7 @@ bool ChannelImpl::publish(const std::string &exchange, const std::string &routin _messageCounter++; // done - return true; + return *_publisher; } /** @@ -703,7 +713,7 @@ bool ChannelImpl::send(const Frame &frame) // added to the list of deferred objects. it will be notified about // the error when the close operation succeeds if (_state == state_closing) return true; - + // are we currently in synchronous mode or are there // other frames waiting for their turn to be sent? if (_synchronous || !_queue.empty()) @@ -826,41 +836,17 @@ void ChannelImpl::reportError(const char *message, bool notifyhandler) } /** - * Process incoming delivery - * - * @param frame The frame to process + * Get the current receiver for a given consumer tag + * @param consumertag the consumer frame + * @return DeferredConsumer */ -void ChannelImpl::process(BasicDeliverFrame &frame) +DeferredConsumer *ChannelImpl::consumer(const std::string &consumertag) const { - // find the consumer for this frame - auto iter = _consumers.find(frame.consumerTag()); - if (iter == _consumers.end()) return; - - // we are going to be receiving a message, store - // the handler for the incoming message - _consumer = iter->second; - - // let the consumer process the frame - _consumer->process(frame); -} - -/** - * Retrieve the current consumer handler - * - * @return The handler responsible for the current message - */ -DeferredConsumerBase *ChannelImpl::consumer() -{ - return _consumer.get(); -} - -/** - * Mark the current consumer as done - */ -void ChannelImpl::complete() -{ - // no more consumer - _consumer.reset(); + // look in the map + auto iter = _consumers.find(consumertag); + + // return the result + return iter == _consumers.end() ? nullptr : iter->second.get(); } /** diff --git a/src/connectioncloseframe.h b/src/connectioncloseframe.h index 17378c0..9828bb5 100644 --- a/src/connectioncloseframe.h +++ b/src/connectioncloseframe.h @@ -82,7 +82,7 @@ public: * @param failingMethod id of the failing method if applicable */ ConnectionCloseFrame(uint16_t code, const std::string &text, uint16_t failingClass = 0, uint16_t failingMethod = 0) : - ConnectionFrame(text.length() + 7), // 1 for extra string byte, 2 for each uint16 + ConnectionFrame((uint32_t)(text.length() + 7)), // 1 for extra string byte, 2 for each uint16 _code(code), _text(text), _failingClass(failingClass), diff --git a/src/connectionimpl.cpp b/src/connectionimpl.cpp index ca81a1f..ab9f781 100644 --- a/src/connectionimpl.cpp +++ b/src/connectionimpl.cpp @@ -3,7 +3,7 @@ * * Implementation of an AMQP connection * - * @copyright 2014 - 2016 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ #include "includes.h" #include "protocolheaderframe.h" @@ -11,6 +11,7 @@ #include "connectioncloseframe.h" #include "reducedbuffer.h" #include "passthroughbuffer.h" +#include "heartbeatframe.h" /** * set namespace @@ -124,7 +125,7 @@ uint64_t ConnectionImpl::parse(const Buffer &buffer) try { // try to recognize the frame - ReducedBuffer reduced_buf(buffer, processed); + ReducedBuffer reduced_buf(buffer, (size_t)processed); ReceivedFrame receivedFrame(reduced_buf, _maxFrame); // do we have the full frame? @@ -145,8 +146,8 @@ uint64_t ConnectionImpl::parse(const Buffer &buffer) // have the initial bytes of the header, we already know how much // data we need for the next frame, otherwise we need at least 7 // bytes for processing the header of the next frame - _expected = receivedFrame.header() ? receivedFrame.totalSize() : 7; - + _expected = receivedFrame.header() ? (uint32_t)receivedFrame.totalSize() : 7; + // we're ready for now return processed; } @@ -267,6 +268,9 @@ void ConnectionImpl::setConnected() { // get the next message const auto &buffer = _queue.front(); + + // data is going to be sent, thus connection is not idle + _idle = false; // send it _handler->onData(_parent, buffer.data(), buffer.size()); @@ -335,6 +339,9 @@ bool ConnectionImpl::send(const Frame &frame) // are we still setting up the connection? if ((_state == state_connected && _queue.empty()) || frame.partOfHandshake()) { + // the data will be sent, so connection is not idle + _idle = false; + // we need an output buffer (this will immediately send the data) PassthroughBuffer buffer(_parent, _handler, frame); } @@ -361,6 +368,9 @@ bool ConnectionImpl::send(CopiedBuffer &&buffer) // are we waiting for other frames to be sent before us? if (_queue.empty()) { + // data will be sent, thus connection is not empty + _idle = false; + // send it directly _handler->onData(_parent, buffer.data(), buffer.size()); } @@ -374,6 +384,24 @@ bool ConnectionImpl::send(CopiedBuffer &&buffer) return true; } +/** + * Send a ping / heartbeat frame to keep the connection alive + * @param force also send heartbeat if connection is not idle + * @return bool + */ +bool ConnectionImpl::heartbeat(bool force) +{ + // do nothing if the connection is not idle (but we do reset the idle state to ensure + // that the next heartbeat will be sent if nothing is going to change from now on) + if (!force && !_idle) return _idle = true; + + // send a frame + if (!send(HeartbeatFrame())) return false; + + // frame has been sent, we treat the connection as idle now until some other data is sent over it + return _idle = true; +} + /** * End of namspace */ diff --git a/src/connectionopenframe.h b/src/connectionopenframe.h index ca78dd6..c063357 100644 --- a/src/connectionopenframe.h +++ b/src/connectionopenframe.h @@ -60,7 +60,7 @@ public: * @param vhost name of virtual host to open */ ConnectionOpenFrame(const std::string &vhost) : - ConnectionFrame(vhost.length() + 3), // length of vhost + byte to encode this length + deprecated shortstring size + deprecated bool + ConnectionFrame((uint32_t)(vhost.length() + 3)), // length of vhost + byte to encode this length + deprecated shortstring size + deprecated bool _vhost(vhost), _deprecatedCapabilities(""), _deprecatedInsist() diff --git a/src/connectionsecureframe.h b/src/connectionsecureframe.h index f8564ec..8c128d1 100644 --- a/src/connectionsecureframe.h +++ b/src/connectionsecureframe.h @@ -43,7 +43,7 @@ public: * @param challenge the challenge */ ConnectionSecureFrame(const std::string& challenge) : - ConnectionFrame(challenge.length() + 4), // 4 for the length of the challenge (uint32_t) + ConnectionFrame((uint32_t)(challenge.length() + 4)), // 4 for the length of the challenge (uint32_t) _challenge(challenge) {} diff --git a/src/connectionsecureokframe.h b/src/connectionsecureokframe.h index d5c907b..7867804 100644 --- a/src/connectionsecureokframe.h +++ b/src/connectionsecureokframe.h @@ -43,7 +43,7 @@ public: * @param response the challenge response */ ConnectionSecureOKFrame(const std::string& response) : - ConnectionFrame(response.length() + 4), //response length + uint32_t for encoding the length + ConnectionFrame((uint32_t)(response.length() + 4)), //response length + uint32_t for encoding the length _response(response) {} diff --git a/src/connectionstartframe.h b/src/connectionstartframe.h index a8a0868..272384e 100644 --- a/src/connectionstartframe.h +++ b/src/connectionstartframe.h @@ -81,7 +81,7 @@ public: * @param locales available locales */ ConnectionStartFrame(uint8_t major, uint8_t minor, const Table& properties, const std::string& mechanisms, const std::string& locales) : - ConnectionFrame((properties.size() + mechanisms.length() + locales.length() + 10)), // 4 for each longstring (size-uint32), 2 major/minor + ConnectionFrame((uint32_t)(properties.size() + mechanisms.length() + locales.length() + 10)), // 4 for each longstring (size-uint32), 2 major/minor _major(major), _minor(minor), _properties(properties), @@ -121,7 +121,7 @@ public: * Major AMQP version number * @return uint8_t */ - uint8_t major() const + uint8_t majorVersion() const { return _major; } @@ -130,7 +130,7 @@ public: * Minor AMQP version number * @return uint8_t */ - uint8_t minor() const + uint8_t minorVersion() const { return _minor; } @@ -196,8 +196,8 @@ public: properties["product"] = "Copernica AMQP library"; properties["version"] = "Unknown"; properties["platform"] = "Unknown"; - properties["copyright"] = "Copyright 2015 Copernica BV"; - properties["information"] = "http://www.copernica.com"; + properties["copyright"] = "Copyright 2015 - 2018 Copernica BV"; + properties["information"] = "https://www.copernica.com"; properties["capabilities"] = capabilities; // move connection to handshake mode diff --git a/src/connectionstartokframe.h b/src/connectionstartokframe.h index 5ca994e..bc46d5e 100644 --- a/src/connectionstartokframe.h +++ b/src/connectionstartokframe.h @@ -82,7 +82,7 @@ public: * @param locale selected locale. */ ConnectionStartOKFrame(const Table& properties, const std::string& mechanism, const std::string& response, const std::string& locale) : - ConnectionFrame((properties.size() + mechanism.length() + response.length() + locale.length() + 6)), // 1 byte extra per shortstring, 4 per longstring + ConnectionFrame((uint32_t)(properties.size() + mechanism.length() + response.length() + locale.length() + 6)), // 1 byte extra per shortstring, 4 per longstring _properties(properties), _mechanism(mechanism), _response(response), diff --git a/src/consumedmessage.h b/src/consumedmessage.h index 01f3bcc..21f4a6c 100644 --- a/src/consumedmessage.h +++ b/src/consumedmessage.h @@ -1,9 +1,14 @@ /** * Base class for a message implementation * - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ +/** + * Dependencies + */ +#include "basicdeliverframe.h" + /** * Set up namespace */ diff --git a/src/deferredconsumer.cpp b/src/deferredconsumer.cpp index 221d089..1c8b83d 100644 --- a/src/deferredconsumer.cpp +++ b/src/deferredconsumer.cpp @@ -3,15 +3,34 @@ * * Implementation file for the DeferredConsumer class * - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ #include "includes.h" +#include "basicdeliverframe.h" /** * Namespace */ namespace AMQP { +/** + * Process a delivery frame + * + * @param frame The frame to process + */ +void DeferredConsumer::process(BasicDeliverFrame &frame) +{ + // this object will handle all future frames with header and body data + _channel->install(shared_from_this()); + + // retrieve the delivery tag and whether we were redelivered + _deliveryTag = frame.deliveryTag(); + _redelivered = frame.redelivered(); + + // initialize the object for the next message + initialize(frame.exchange(), frame.routingKey()); +} + /** * Report success for frames that report start consumer operations * @param name Consumer tag that is started @@ -32,18 +51,6 @@ const std::shared_ptr &DeferredConsumer::reportSuccess(const std::stri return _next; } -/** - * Announce that a message was received - * @param message The message to announce - * @param deliveryTag The delivery tag (for ack()ing) - * @param redelivered Is this a redelivered message - */ -void DeferredConsumer::announce(const Message &message, uint64_t deliveryTag, bool redelivered) const -{ - // simply execute the message callback - _messageCallback(message, deliveryTag, redelivered); -} - /** * End namespace */ diff --git a/src/deferredconsumerbase.cpp b/src/deferredconsumerbase.cpp deleted file mode 100644 index 240842b..0000000 --- a/src/deferredconsumerbase.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/** - * deferredconsumerbase.cpp - * - * Base class for the deferred consumer and the - * deferred get. - * - * @copyright 2016 - 2017 Copernica B.V. - */ - -/** - * Dependencies - */ -#include "../include/deferredconsumerbase.h" -#include "basicdeliverframe.h" -#include "basicgetokframe.h" -#include "basicheaderframe.h" -#include "bodyframe.h" - -/** - * Start namespace - */ -namespace AMQP { - -/** - * Process a delivery frame - * - * @param frame The frame to process - */ -void DeferredConsumerBase::process(BasicDeliverFrame &frame) -{ - // retrieve the delivery tag and whether we were redelivered - _deliveryTag = frame.deliveryTag(); - _redelivered = frame.redelivered(); - - // anybody interested in the new message? - if (_beginCallback) _beginCallback(); - - // do we have anybody interested in messages? - if (_messageCallback) _message.construct(frame.exchange(), frame.routingKey()); -} - -/** - * Process a delivery frame from a get request - * - * @param frame The frame to process - */ -void DeferredConsumerBase::process(BasicGetOKFrame &frame) -{ - // retrieve the delivery tag and whether we were redelivered - _deliveryTag = frame.deliveryTag(); - _redelivered = frame.redelivered(); - - // anybody interested in the new message? - if (_beginCallback) _beginCallback(); - - // do we have anybody interested in messages? - if (_messageCallback) _message.construct(frame.exchange(), frame.routingKey()); -} - -/** - * Process the message headers - * - * @param frame The frame to process - */ -void DeferredConsumerBase::process(BasicHeaderFrame &frame) -{ - // store the body size - _bodySize = frame.bodySize(); - - // do we have a message? - if (_message) - { - // store the body size and metadata - _message->setBodySize(_bodySize); - _message->set(frame.metaData()); - } - - // anybody interested in the headers? - if (_headerCallback) _headerCallback(frame.metaData()); - - // no body data expected? then we are now complete - if (!_bodySize) complete(); -} - -/** - * Process the message data - * - * @param frame The frame to process - */ -void DeferredConsumerBase::process(BodyFrame &frame) -{ - // make sure we stay in scope - auto self = shared_from_this(); - - // update the bytes still to receive - _bodySize -= frame.payloadSize(); - - // anybody interested in the data? - if (_dataCallback) _dataCallback(frame.payload(), frame.payloadSize()); - - // do we have a message? then append the data - if (_message) _message->append(frame.payload(), frame.payloadSize()); - - // if all bytes were received we are now complete - if (!_bodySize) complete(); -} - -/** - * Indicate that a message was done - */ -void DeferredConsumerBase::complete() -{ - // make sure we stay in scope - auto self = shared_from_this(); - - // also monitor the channel - Monitor monitor{ _channel }; - - // do we have a message? - if (_message) - { - // announce the message - announce(*_message, _deliveryTag, _redelivered); - - // and destroy it - _message.reset(); - } - - // do we have to inform anyone about completion? - if (_completeCallback) _completeCallback(_deliveryTag, _redelivered); - - // do we still have a valid channel - if (!monitor.valid()) return; - - // we are now done executing - _channel->complete(); -} - -/** - * End namespace - */ -} diff --git a/src/deferredextreceiver.cpp b/src/deferredextreceiver.cpp new file mode 100644 index 0000000..0f2f047 --- /dev/null +++ b/src/deferredextreceiver.cpp @@ -0,0 +1,64 @@ +/** + * DeferredExtReceiver.cpp + * + * Implementation file for the DeferredExtReceiver class + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Dependencies + */ +#include "amqpcpp/deferredextreceiver.h" +#include "amqpcpp/channelimpl.h" + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * Initialize the object to send out a message + * @param exchange the exchange to which the message was published + * @param routingkey the routing key that was used to publish the message + */ +void DeferredExtReceiver::initialize(const std::string &exchange, const std::string &routingkey) +{ + // call base + DeferredReceiver::initialize(exchange, routingkey); + + // do we have anybody interested in messages? in that case we construct the message + if (_messageCallback) _message.construct(exchange, routingkey); +} + +/** + * Indicate that a message was done + */ +void DeferredExtReceiver::complete() +{ + // also monitor the channel + Monitor monitor(_channel); + + // do we have a message? + if (_message) _messageCallback(*_message, _deliveryTag, _redelivered); + + // do we have to inform anyone about completion? + if (_deliveredCallback) _deliveredCallback(_deliveryTag, _redelivered); + + // for the next iteration we want a new message + _message.reset(); + + // do we still have a valid channel + if (!monitor.valid()) return; + + // we are now done executing, so the channel can forget the current receiving object + _channel->install(nullptr); +} + +/** + * End of namespace + */ +} + + diff --git a/src/deferredget.cpp b/src/deferredget.cpp index dbd838b..e8985f1 100644 --- a/src/deferredget.cpp +++ b/src/deferredget.cpp @@ -4,7 +4,7 @@ * Implementation of the DeferredGet call * * @author Emiel Bruijntjes - * @copyright 2014 - 2017 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -19,23 +19,22 @@ namespace AMQP { /** * Report success for a get operation - * * @param messagecount Number of messages left in the queue * @param deliveryTag Delivery tag of the message coming in * @param redelivered Was the message redelivered? */ const std::shared_ptr &DeferredGet::reportSuccess(uint32_t messagecount, uint64_t deliveryTag, bool redelivered) { + // install this object as the handler for the upcoming header and body frames + _channel->install(shared_from_this()); + // store delivery tag and redelivery status _deliveryTag = deliveryTag; _redelivered = redelivered; - // install ourselves in the channel - _channel->install("", shared_from_this(), true); - // report the size (note that this is the size _minus_ the message that is retrieved // (and for which the callback will be called later), so it could be zero) - if (_sizeCallback) _sizeCallback(messagecount); + if (_countCallback) _countCallback(messagecount); // return next handler return _next; @@ -48,7 +47,7 @@ const std::shared_ptr &DeferredGet::reportSuccess(uint32_t messagecoun const std::shared_ptr &DeferredGet::reportSuccess() const { // report the size - if (_sizeCallback) _sizeCallback(0); + if (_countCallback) _countCallback(0); // check if a callback was set if (_emptyCallback) _emptyCallback(); @@ -58,27 +57,15 @@ const std::shared_ptr &DeferredGet::reportSuccess() const } /** - * Announce that a message has been received - * @param message The message to announce - * @param deliveryTag The delivery tag (for ack()ing) - * @param redelivered Is this a redelivered message + * Extended implementation of the complete method that is called when a message was fully received */ -void DeferredGet::announce(const Message &message, uint64_t deliveryTag, bool redelivered) const +void DeferredGet::complete() { - // monitor the channel - Monitor monitor{ _channel }; - - // the channel is now synchronized + // the channel is now synchronized, delayed frames may now be sent _channel->onSynchronized(); - - // simply execute the message callback - _messageCallback(std::move(message), deliveryTag, redelivered); - - // check if the channel is still valid - if (!monitor.valid()) return; - - // stop consuming now - _channel->uninstall({}); + + // pass on to normal implementation + DeferredExtReceiver::complete(); } /** diff --git a/src/deferredpublisher.cpp b/src/deferredpublisher.cpp new file mode 100644 index 0000000..b75e6df --- /dev/null +++ b/src/deferredpublisher.cpp @@ -0,0 +1,72 @@ +/** + * DeferredPublisher.cpp + * + * Implementation file for the DeferredPublisher class + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ +#include "includes.h" +#include "basicreturnframe.h" + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * Process a return frame + * + * @param frame The frame to process + */ +void DeferredPublisher::process(BasicReturnFrame &frame) +{ + // this object will handle all future frames with header and body data + _channel->install(shared_from_this()); + + // retrieve the delivery tag and whether we were redelivered + _code = frame.replyCode(); + _description = frame.replyText(); + + // notify user space of the begin of the returned message + if (_beginCallback) _beginCallback(_code, _description); + + // initialize the object for the next message + initialize(frame.exchange(), frame.routingKey()); + + // do we have anybody interested in messages? in that case we construct the message + if (_bounceCallback) _message.construct(frame.exchange(), frame.routingKey()); +} + +/** + * Indicate that a message was done + */ +void DeferredPublisher::complete() +{ + // also monitor the channel + Monitor monitor(_channel); + + // do we have a message? + if (_message) _bounceCallback(*_message, _code, _description); + + // do we have to inform anyone about completion? + if (_completeCallback) _completeCallback(); + + // for the next iteration we want a new message + _message.reset(); + + // the description can be thrown away too + _description.clear(); + + // do we still have a valid channel + if (!monitor.valid()) return; + + // we are now done executing, so the channel can forget the current receiving object + _channel->install(nullptr); +} + +/** + * End of namespace + */ +} + diff --git a/src/deferredreceiver.cpp b/src/deferredreceiver.cpp new file mode 100644 index 0000000..f4e246c --- /dev/null +++ b/src/deferredreceiver.cpp @@ -0,0 +1,91 @@ +/** + * DeferredReceiver.cpp + * + * Implementation file for the DeferredReceiver class + * + * @copyright 2016 - 2018 Copernica B.V. + */ + +/** + * Dependencies + */ +#include "amqpcpp/deferredreceiver.h" +#include "basicdeliverframe.h" +#include "basicgetokframe.h" +#include "basicheaderframe.h" +#include "bodyframe.h" + +/** + * Start namespace + */ +namespace AMQP { + +/** + * Initialize the object: we are going to receive a message, next frames will be header and data + * @param exchange + * @param routingkey + */ +void DeferredReceiver::initialize(const std::string &exchange, const std::string &routingkey) +{ + // anybody interested in the new message? + if (_startCallback) _startCallback(exchange, routingkey); +} + +/** + * Process the message headers + * + * @param frame The frame to process + */ +void DeferredReceiver::process(BasicHeaderFrame &frame) +{ + // make sure we stay in scope + auto self = lock(); + + // store the body size + _bodySize = frame.bodySize(); + + // is user interested in the size? + if (_sizeCallback) _sizeCallback(_bodySize); + + // do we have a message? + if (_message) + { + // store the body size and metadata + _message->setBodySize(_bodySize); + _message->set(frame.metaData()); + } + + // anybody interested in the headers? + if (_headerCallback) _headerCallback(frame.metaData()); + + // no body data expected? then we are now complete + if (_bodySize == 0) complete(); +} + +/** + * Process the message data + * + * @param frame The frame to process + */ +void DeferredReceiver::process(BodyFrame &frame) +{ + // make sure we stay in scope + auto self = lock(); + + // update the bytes still to receive + _bodySize -= frame.payloadSize(); + + // anybody interested in the data? + if (_dataCallback) _dataCallback(frame.payload(), frame.payloadSize()); + + // do we have a message? then append the data + if (_message) _message->append(frame.payload(), frame.payloadSize()); + + // if all bytes were received we are now complete + if (_bodySize == 0) complete(); +} + +/** + * End namespace + */ +} diff --git a/src/exchangebindframe.h b/src/exchangebindframe.h index 1d01161..0b6a092 100644 --- a/src/exchangebindframe.h +++ b/src/exchangebindframe.h @@ -95,7 +95,7 @@ public: * @param arguments */ ExchangeBindFrame(uint16_t channel, const std::string &destination, const std::string &source, const std::string &routingKey, bool noWait, const Table &arguments) : - ExchangeFrame(channel, (destination.length() + source.length() + routingKey.length() + arguments.size() + 6)), // 1 for each string, 1 for booleanset, 2 for deprecated field + ExchangeFrame(channel, (uint32_t)(destination.length() + source.length() + routingKey.length() + arguments.size() + 6)), // 1 for each string, 1 for booleanset, 2 for deprecated field _destination(destination), _source(source), _routingKey(routingKey), diff --git a/src/exchangedeclareframe.h b/src/exchangedeclareframe.h index aa940dd..5e02b2c 100644 --- a/src/exchangedeclareframe.h +++ b/src/exchangedeclareframe.h @@ -1,7 +1,7 @@ /** * Class describing an AMQP exchange declare frame * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ /** @@ -77,16 +77,17 @@ public: * @param type exchange type * @param passive do not create exchange if it does not exist * @param durable durable exchange + * @param autodelete is this an auto-delete exchange? + * @param internal is this an internal exchange * @param noWait do not wait on response * @param arguments additional arguments */ - ExchangeDeclareFrame(uint16_t channel, const std::string& name, const std::string& type, bool passive, bool durable, bool noWait, const Table& arguments) : - ExchangeFrame(channel, (name.length() + type.length() + arguments.size() + 5)), // size of name, type and arguments + 1 (all booleans are stored in 1 byte) + 2 (deprecated short) + 2 (string sizes) + ExchangeDeclareFrame(uint16_t channel, const std::string& name, const char *type, bool passive, bool durable, bool autodelete, bool internal, bool nowait, const Table& arguments) : + ExchangeFrame(channel, (uint32_t)(name.length() + strlen(type) + arguments.size() + 5)), // size of name, type and arguments + 1 (all booleans are stored in 1 byte) + 2 (deprecated short) + 2 (string sizes) _name(name), _type(type), - _bools(passive, durable, false, false, noWait), - _arguments(arguments) - {} + _bools(passive, durable, autodelete, internal, nowait), + _arguments(arguments) {} /** * Construct parsing a declare frame from a received frame @@ -162,6 +163,24 @@ public: { return _bools.get(1); } + + /** + * Is this an autodelete exchange? + * @return bool + */ + bool autoDelete() const + { + return _bools.get(2); + } + + /** + * Is this an internal exchange? + * @return bool + */ + bool internal() const + { + return _bools.get(3); + } /** * Do not wait for a response diff --git a/src/exchangedeleteframe.h b/src/exchangedeleteframe.h index cdd906f..2fc9e23 100644 --- a/src/exchangedeleteframe.h +++ b/src/exchangedeleteframe.h @@ -73,7 +73,7 @@ public: * @param bool noWait Do not wait for a response */ ExchangeDeleteFrame(uint16_t channel, const std::string& name, bool ifUnused = false, bool noWait = false) : - ExchangeFrame(channel, name.length() + 4), // length of the name, 1 byte for encoding this length, 1 for bools, 2 for deprecated short + ExchangeFrame(channel, (uint32_t)(name.length() + 4)), // length of the name, 1 byte for encoding this length, 1 for bools, 2 for deprecated short _name(name), _bools(ifUnused, noWait) {} diff --git a/src/exchangeunbindframe.h b/src/exchangeunbindframe.h index d92d941..bfd3469 100644 --- a/src/exchangeunbindframe.h +++ b/src/exchangeunbindframe.h @@ -95,7 +95,7 @@ public: * @param arguments */ ExchangeUnbindFrame(uint16_t channel, const std::string &destination, const std::string &source, const std::string &routingKey, bool noWait, const Table &arguments) : - ExchangeFrame(channel, (destination.length() + source.length() + routingKey.length() + arguments.size() + 6)), // 1 for each string, 1 for booleanset, 2 for deprecated field + ExchangeFrame(channel, (uint32_t)(destination.length() + source.length() + routingKey.length() + arguments.size() + 6)), // 1 for each string, 1 for booleanset, 2 for deprecated field _destination(destination), _source(source), _routingKey(routingKey), diff --git a/src/extframe.h b/src/extframe.h index d6de772..513c674 100644 --- a/src/extframe.h +++ b/src/extframe.h @@ -19,8 +19,8 @@ /** * Dependencies */ -#include "../include/frame.h" -#include "../include/receivedframe.h" +#include "amqpcpp/frame.h" +#include "amqpcpp/receivedframe.h" /** * Set up namespace diff --git a/src/flags.cpp b/src/flags.cpp index 946b51c..24351cb 100644 --- a/src/flags.cpp +++ b/src/flags.cpp @@ -3,7 +3,7 @@ * * The various flags that are supported * - * @copyright 2014 Copernica BV + * @copyright 2014 - 2018 Copernica BV */ #include "includes.h" @@ -32,6 +32,7 @@ const int immediate = 0x1000; const int redelivered = 0x2000; const int multiple = 0x4000; const int requeue = 0x8000; +const int internal = 0x10000; /** * Flags for event loops diff --git a/src/framecheck.h b/src/framecheck.h index ab8c959..675cebe 100644 --- a/src/framecheck.h +++ b/src/framecheck.h @@ -50,7 +50,7 @@ public: virtual ~FrameCheck() { // update the number of bytes to skip - _frame->_skip += _size; + _frame->_skip += (uint32_t)_size; } }; diff --git a/src/heartbeatframe.h b/src/heartbeatframe.h index 955c57d..cd4320b 100644 --- a/src/heartbeatframe.h +++ b/src/heartbeatframe.h @@ -59,9 +59,6 @@ public: { // notify the connection-handler connection->reportHeartbeat(); - - // send back the same frame - connection->send(*this); // done return true; diff --git a/src/includes.h b/src/includes.h index b105932..795d501 100644 --- a/src/includes.h +++ b/src/includes.h @@ -9,7 +9,7 @@ // c and c++ dependencies #include -#include +#include // TODO cstring #include #include #include @@ -21,67 +21,70 @@ #include #include #include -#include -#include -#include -#include -#include + +#include // TODO is this needed + #include #include +// TODO make this nice +#ifdef _MSC_VER +//not #if defined(_WIN32) || defined(_WIN64) because we have strncasecmp in mingw +#define strncasecmp _strnicmp +#define strcasecmp _stricmp +#endif + // forward declarations -#include "../include/classes.h" +#include "amqpcpp/classes.h" // utility classes -#include "../include/endian.h" -#include "../include/buffer.h" -#include "../include/bytebuffer.h" -#include "../include/receivedframe.h" -#include "../include/outbuffer.h" -#include "../include/copiedbuffer.h" -#include "../include/watchable.h" -#include "../include/monitor.h" -#include "../include/tcpdefines.h" +#include "amqpcpp/endian.h" +#include "amqpcpp/buffer.h" +#include "amqpcpp/bytebuffer.h" +#include "amqpcpp/receivedframe.h" +#include "amqpcpp/outbuffer.h" +#include "amqpcpp/copiedbuffer.h" +#include "amqpcpp/watchable.h" +#include "amqpcpp/monitor.h" // amqp types -#include "../include/field.h" -#include "../include/numericfield.h" -#include "../include/decimalfield.h" -#include "../include/stringfield.h" -#include "../include/booleanset.h" -#include "../include/fieldproxy.h" -#include "../include/table.h" -#include "../include/array.h" +#include "amqpcpp/field.h" +#include "amqpcpp/numericfield.h" +#include "amqpcpp/decimalfield.h" +#include "amqpcpp/stringfield.h" +#include "amqpcpp/booleanset.h" +#include "amqpcpp/fieldproxy.h" +#include "amqpcpp/table.h" +#include "amqpcpp/array.h" // envelope for publishing and consuming -#include "../include/metadata.h" -#include "../include/envelope.h" -#include "../include/message.h" +#include "amqpcpp/metadata.h" +#include "amqpcpp/envelope.h" +#include "amqpcpp/message.h" // mid level includes -#include "../include/exchangetype.h" -#include "../include/flags.h" -#include "../include/callbacks.h" -#include "../include/deferred.h" -#include "../include/deferredconsumer.h" -#include "../include/deferredqueue.h" -#include "../include/deferreddelete.h" -#include "../include/deferredcancel.h" -#include "../include/deferredget.h" -#include "../include/channelimpl.h" -#include "../include/channel.h" -#include "../include/login.h" -#include "../include/address.h" -#include "../include/connectionhandler.h" -#include "../include/connectionimpl.h" -#include "../include/connection.h" -#include "../include/tcphandler.h" -#include "../include/tcpconnection.h" +#include "amqpcpp/exchangetype.h" +#include "amqpcpp/flags.h" +#include "amqpcpp/callbacks.h" +#include "amqpcpp/deferred.h" +#include "amqpcpp/deferredconsumer.h" +#include "amqpcpp/deferredpublisher.h" +#include "amqpcpp/deferredqueue.h" +#include "amqpcpp/deferreddelete.h" +#include "amqpcpp/deferredcancel.h" +#include "amqpcpp/deferredget.h" +#include "amqpcpp/channelimpl.h" +#include "amqpcpp/channel.h" +#include "amqpcpp/login.h" +#include "amqpcpp/address.h" +#include "amqpcpp/connectionhandler.h" +#include "amqpcpp/connectionimpl.h" +#include "amqpcpp/connection.h" // classes that are very commonly used -#include "../include/exception.h" -#include "../include/protocolexception.h" -#include "../include/frame.h" +#include "amqpcpp/exception.h" +#include "amqpcpp/protocolexception.h" +#include "amqpcpp/frame.h" #include "extframe.h" #include "methodframe.h" #include "headerframe.h" @@ -92,6 +95,5 @@ #include "basicframe.h" #include "confirmframe.h" #include "transactionframe.h" -#include "addressinfo.h" diff --git a/src/linux_tcp/CMakeLists.txt b/src/linux_tcp/CMakeLists.txt new file mode 100644 index 0000000..55e04c9 --- /dev/null +++ b/src/linux_tcp/CMakeLists.txt @@ -0,0 +1,18 @@ +add_sources( + addressinfo.h + includes.h + openssl.cpp + openssl.h + pipe.h + sslconnected.h + sslcontext.h + sslhandshake.h + sslwrapper.h + tcpclosed.h + tcpconnected.h + tcpconnection.cpp + tcpinbuffer.h + tcpoutbuffer.h + tcpresolver.h + tcpstate.h +) diff --git a/src/addressinfo.h b/src/linux_tcp/addressinfo.h similarity index 100% rename from src/addressinfo.h rename to src/linux_tcp/addressinfo.h diff --git a/src/linux_tcp/function.h b/src/linux_tcp/function.h new file mode 100644 index 0000000..899f536 --- /dev/null +++ b/src/linux_tcp/function.h @@ -0,0 +1,173 @@ +/** + * Function.h + * + * When you want to call a function only if it is already linked into + * the program space. you would normally use the dlsym() function for + * that. Th Function object in this file is a little more convenient: + * + * // get the function object + * Function func func("example_function"); + * + * // call the function + * int result = func(123); + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Dependencies + */ +#include +#include +#include + +/** + * Namespace + */ +namespace AMQP { + +/** + * Make the Function class a templated class + */ +template class Function {}; + +/** + * So that we can write a partial specialisation + * using a function prototype, indicating the + * prototype usable for the given callback + */ +template +class Function +{ +private: + /** + * Store pointer to the actual dynamically loaded + * function. This is done in a union because the + * result from a dlsym call is a void pointer which + * cannot be legally cast into a function pointer. + * + * We therefore always set the first type (which is + * void* making it legal) and, because all members + * in a union share the same address, we can then + * read the second type and actually call it. + * + * @var Callable + */ + union Callable { + + /** + * Property for getting and setting the return + * value from dlsym. This is always a void* + * @var void + */ + void *ptr; + + /** + * Property for executing the mapped function + * + * @param mixed,... function parameters + * @return mixed + * + * @var function + */ + T (*func)(Arguments...); + + + /** + * Constructor + */ + Callable() : ptr(nullptr) {} + + /** + * We may be moved + * + * @param callable the callable we are moving + */ + Callable(Callable&& callable) : + ptr(callable.ptr) + { + // the other callable no longer has a handle + callable.ptr = nullptr; + } + + /** + * Copy construtor + * @param callable the callable we are moving + */ + Callable(const Callable &callable) : + ptr(callable.ptr) {} + + /** + * Constructor + * + * @param function the mapped function + */ + Callable(void *function) : ptr(function) {} + + } _method; + +public: + /** + * Constructor + * @param handle Handle to access openssl + * @param name Name of the function + */ + Function(void *handle, const char *name) : + _method(dlsym(handle, name)) {} + + /** + * Destructor + */ + virtual ~Function() {} + + /** + * Is this a valid function or not? + * @return bool + */ + bool valid() const + { + return _method.ptr != nullptr; + } + + /** + * The library object can also be used as in a boolean context, + * for that there is an implementation of a casting operator, and + * the negate operator + * @return bool + */ + operator bool () const { return valid(); } + bool operator ! () const { return !valid(); } + + /** + * Test whether we are a valid object + * @param nullptr test if we are null + */ + bool operator==(std::nullptr_t /* nullptr */) const { return !valid(); } + bool operator!=(std::nullptr_t /* nullptr */) const { return valid(); } + + /** + * Invoke the function + * + * @param mixed,... + */ + T operator()(Arguments... parameters) const + { + // check whether we have a valid function + if (!valid()) throw std::bad_function_call(); + + // execute the method given all the parameters + return _method.func(std::forward(parameters)...); + } +}; + +/** + * End of namespace + */ +} + diff --git a/src/linux_tcp/includes.h b/src/linux_tcp/includes.h new file mode 100644 index 0000000..a83e9bd --- /dev/null +++ b/src/linux_tcp/includes.h @@ -0,0 +1,27 @@ +/** + * Includes.h + * + * The includes that are necessary to compile the optional TCP part of the AMQP library + * This file also holds includes that may not be necessary for including the library + * + * @documentation private + */ +// include files from main library +#include "../includes.h" + +// c and c++ dependencies +#include +#include +#include +#include +#include + +// utility classes +#include "amqpcpp/linux_tcp/tcpdefines.h" + +// mid level includes +#include "amqpcpp/linux_tcp/tcphandler.h" +#include "amqpcpp/linux_tcp/tcpconnection.h" + +// classes that are very commonly used +#include "addressinfo.h" diff --git a/src/linux_tcp/openssl.cpp b/src/linux_tcp/openssl.cpp new file mode 100644 index 0000000..2afc378 --- /dev/null +++ b/src/linux_tcp/openssl.cpp @@ -0,0 +1,317 @@ +/** + * OpenSSL.cpp + * + * Implementation file for the openssl.h header file + * + * @copyright 2018 Copernica BV + */ + +/** + * Dependencies + */ +#include "openssl.h" +#include "function.h" +#include "amqpcpp/openssl.h" + +/** + * The handle to access openssl (the result of dlopen("openssl.so")) + * By default we set this to RTLD_DEFAULT, so that AMQP-CPP checks the internal process space + */ +static void *handle = RTLD_DEFAULT; + +/** + * Just the AMQP namespace + */ +namespace AMQP { + +/** + * Function to set the openssl handle + * @param ptr + */ +void openssl(void *ptr) +{ + // store handle + handle = ptr; +} + +/** + * Begin of AMQP::OpenSSL namespace + */ +namespace OpenSSL { + +/** + * Is the openssl library loaded? + * @return bool + */ +bool valid() +{ + // create a function + static Function func(handle, "SSL_CTX_new"); + + // we need a library + return func; +} + +/** + * Get the SSL_METHOD for outgoing connections + * @return SSL_METHOD * + */ +const SSL_METHOD *TLS_client_method() +{ + // create a function that loads the method + static Function func(handle, "TLS_client_method"); + + // call the openssl function + if (func) return func(); + + // older openssl libraries do not have this function, so we try to load an other function + static Function old(handle, "SSLv23_client_method"); + + // call the old one + return old(); +} + +/** + * Create new SSL context + * @param method SSL_METHOD can be of the following types: TLS_method(), TLS_server_method(), TLS_client_method() + * @return pointer to object + */ +SSL_CTX *SSL_CTX_new(const SSL_METHOD *method) +{ + // create a function + static Function func(handle, "SSL_CTX_new"); + + // call the openssl function + return func(method); +} + +/** + * Read data from an ssl socket + * @param ssl ssl structure + * @param buf buffer to read into + * @param num size of buffer + * @return int number of bytes read + */ +int SSL_read(SSL *ssl, void *buf, int num) +{ + // create a function + static Function func(handle, "SSL_read"); + + // call the openssl function + return func(ssl, buf, num); +} + +/** + * Read data from an ssl socket + * @param ssl ssl structure + * @param buf buffer to write + * @param num size of buffer + * @return int number of bytes written + */ +int SSL_write(SSL *ssl, const void *buf, int num) +{ + // create a function + static Function func(handle, "SSL_write"); + + // call the openssl function + return func(ssl, buf, num); +} + +/** + * Connect the SSL object with a file descriptor + * @param ssl SSL object + * @param fd file descriptor + * @return int wether the operation succeeded or not + */ +int SSL_set_fd(SSL *ssl, int fd) +{ + // create a function + static Function func(handle, "SSL_set_fd"); + + // call the openssl function + return func(ssl, fd); +} + +/** + * The number of bytes availabe in the ssl struct that have been read + * from the socket, but that have not been returned the SSL_read() + * @param ssl SSL object + * @return int number of bytes + */ +int SSL_pending(const SSL *ssl) +{ + // create a function + static Function func(handle, "SSL_pending"); + + // call the openssl function + return func(ssl); +} + +/** + * Free an allocated ssl context + * @param ctx + */ +void SSL_CTX_free(SSL_CTX *ctx) +{ + // create a function + static Function func(handle, "SSL_CTX_free"); + + // call the openssl function + return func(ctx); +} + +/** + * Free an allocated SSL structure + * @param ssl SSL object to be freed + * @return int wether the operation succeeded or not + */ +void SSL_free(SSL *ssl) +{ + // create a function + static Function func(handle, "SSL_free"); + + // call the openssl function + return func(ssl); +} + +/** + * Create a new SSL structure for a connection + * @param ctx SSL context object + * @return SSL the created SSL oject based on th context + */ +SSL *SSL_new(SSL_CTX *ctx) +{ + // create a function + static Function func(handle, "SSL_new"); + + // call the openssl function + return func(ctx); +} + +/** + * Increment refcount for a ssl structure + * @param ctx SSL structure + * @return int 1 for success, 0 for failure + */ +int SSL_up_ref(SSL *ssl) +{ + // create a function + static Function func(handle, "SSL_up_ref"); + + // call the openssl function if it exists + if (func) return func(ssl); + + // @todo use our own implementation + + return 0; +} + +/** + * Shut down a TLS/SSL shut down + * @param ssl SSL object to terminate + * @return int returns diagnostic values + */ +int SSL_shutdown(SSL *ssl) +{ + // create a function + static Function func(handle, "SSL_shutdown"); + + // call the openssl function + return func(ssl); +} + +/** + * Prepare SSL object to work in client or server mode + * @param ssl SSL object to set connect state on + */ +void SSL_set_connect_state(SSL *ssl) +{ + // create a function + static Function func(handle, "SSL_set_connect_state"); + + // call the openssl function + func(ssl); + +} + +/** + * Perform a TLS/SSL handshake + * @param ssl SSL object + * @return int returns diagnostic values + */ +int SSL_do_handshake(SSL *ssl) +{ + // create a function + static Function func(handle, "SSL_do_handshake"); + + // call the openssl function + return func(ssl); +} + +/** + * Obtain shutdown statue for TLS/SSL I/O operation + * @param ssl SSL object + * @return int returns error values + */ +int SSL_get_shutdown(const SSL *ssl) +{ + // create a function + static Function func(handle, "SSL_get_shutdown"); + + // call the openssl function + return func(ssl); +} + +/** + * Obtain result code for TLS/SSL I/O operation + * @param ssl SSL object + * @param ret the returned diagnostic value of SSL calls + * @return int returns error values + */ +int SSL_get_error(const SSL *ssl, int ret) +{ + // create a function + static Function func(handle, "SSL_get_error"); + + // call the openssl function + return func(ssl, ret); +} + +/** + * Internal handling function for a ssl context + * @param ssl ssl context + * @param cmd command + * @param larg first arg + * @param parg second arg + * @return long + */ +long SSL_ctrl(SSL *ssl, int cmd, long larg, void *parg) +{ + // create a function + static Function func(handle, "SSL_ctrl"); + + // call the openssl function + return func(ssl, cmd, larg, parg); +} + +/** + * Set the certificate file to be used by the connection + * @param ssl ssl structure + * @param file filename + * @param type type of file + * @return int + */ +int SSL_use_certificate_file(SSL *ssl, const char *file, int type) +{ + // create a function + static Function func(handle, "SSL_use_certificate_file"); + + // call the openssl function + return func(ssl, file, type); +} + +/** + * End of namespace + */ +}} + diff --git a/src/linux_tcp/openssl.h b/src/linux_tcp/openssl.h new file mode 100644 index 0000000..e3461e2 --- /dev/null +++ b/src/linux_tcp/openssl.h @@ -0,0 +1,57 @@ +/** + * OpenSSL.h + * + * Header file in which we list all openssl functions in our own namespace + * that we call instead of the actual openssl functions. This allows us to + * intercept the calls and forward them to a dynamically loaded namespace + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Dependencies + */ +#include + +/** + * Begin of namespace + */ +namespace AMQP { namespace OpenSSL { + +/** + * Function to check if openssl is loaded + * @return bool + */ +bool valid(); + +/** + * List of all wrapper methods that are in use inside AMQP-CPP + */ +const SSL_METHOD *TLS_client_method(); +SSL_CTX *SSL_CTX_new(const SSL_METHOD *method); +SSL *SSL_new(SSL_CTX *ctx); +int SSL_do_handshake(SSL *ssl); +int SSL_read(SSL *ssl, void *buf, int num); +int SSL_write(SSL *ssl, const void *buf, int num); +int SSL_shutdown(SSL *ssl); +int SSL_pending(const SSL *ssl); +int SSL_set_fd(SSL *ssl, int fd); +int SSL_get_shutdown(const SSL *ssl); +int SSL_get_error(const SSL *ssl, int ret); +int SSL_use_certificate_file(SSL *ssl, const char *file, int type); +void SSL_set_connect_state(SSL *ssl); +void SSL_CTX_free(SSL_CTX *ctx); +void SSL_free(SSL *ssl); +long SSL_ctrl(SSL *ssl, int cmd, long larg, void *parg); + +/** + * End of namespace + */ +}} + diff --git a/src/pipe.h b/src/linux_tcp/pipe.h similarity index 100% rename from src/pipe.h rename to src/linux_tcp/pipe.h diff --git a/src/linux_tcp/sslconnected.h b/src/linux_tcp/sslconnected.h new file mode 100644 index 0000000..f745e36 --- /dev/null +++ b/src/linux_tcp/sslconnected.h @@ -0,0 +1,512 @@ +/** + * SslConnected.h + * + * The actual tcp connection over SSL + * + * @copyright 2018 copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Dependencies + */ +#include "tcpoutbuffer.h" +#include "tcpinbuffer.h" +#include "wait.h" +#include "sslwrapper.h" +#include "sslshutdown.h" + +/** + * Set up namespace + */ +namespace AMQP { + +/** + * Class definition + */ +class SslConnected : public TcpState, private Watchable +{ +private: + /** + * The SSL structure + * @var SslWrapper + */ + SslWrapper _ssl; + + /** + * Socket file descriptor + * @var int + */ + int _socket; + + /** + * The outgoing buffer + * @var TcpBuffer + */ + TcpOutBuffer _out; + + /** + * The incoming buffer + * @var TcpInBuffer + */ + TcpInBuffer _in; + + /** + * Are we now busy with sending or receiving? + * @var enum + */ + enum State { + state_idle, + state_sending, + state_receiving + } _state; + + /** + * Should we close the connection after we've finished all operations? + * @var bool + */ + bool _closed = false; + + /** + * Have we reported the final instruction to the user? + * @var bool + */ + bool _finalized = false; + + /** + * Cached reallocation instruction + * @var size_t + */ + size_t _reallocate = 0; + + + /** + * Close the connection + * @return bool + */ + bool close() + { + // do nothing if already closed + if (_socket < 0) return false; + + // and stop monitoring it + _handler->monitor(_connection, _socket, 0); + + // close the socket + ::close(_socket); + + // forget filedescriptor + _socket = -1; + + // done + return true; + } + + /** + * Construct the final state + * @param monitor Object that monitors whether connection still exists + * @return TcpState* + */ + TcpState *finalstate(const Monitor &monitor) + { + // close the socket if it is still open + close(); + + // if the object is still in a valid state, we can move to the close-state, + // otherwise there is no point in moving to a next state + return monitor.valid() ? new TcpClosed(this) : nullptr; + } + + /** + * Proceed with the next operation after the previous operation was + * a success, possibly changing the filedescriptor-monitor + * @return TcpState* + */ + TcpState *proceed() + { + // if we still have an outgoing buffer we want to send out data + if (_out) + { + // let's wait until the socket becomes writable + _handler->monitor(_connection, _socket, readable | writable); + } + else if (_closed) + { + // start the state that closes the connection + auto *nextstate = new SslShutdown(_connection, _socket, std::move(_ssl), _finalized, _handler); + + // we forget the current socket to prevent that it gets destructed + _socket = -1; + + // report the next state + return nextstate; + } + else + { + // let's wait until the socket becomes readable + _handler->monitor(_connection, _socket, readable); + } + + // done + return this; + } + + /** + * Method to repeat the previous call\ + * @param monitor monitor to check if connection object still exists + * @param state the state that we were in + * @param result result of an earlier SSL_get_error call + * @return TcpState* + */ + TcpState *repeat(const Monitor &monitor, enum State state, int error) + { + // check the error + switch (error) { + case SSL_ERROR_WANT_READ: + // remember state + _state = state; + + // the operation must be repeated when readable + _handler->monitor(_connection, _socket, readable); + + // allow chaining + return monitor.valid() ? this : nullptr; + + case SSL_ERROR_WANT_WRITE: + // remember state + _state = state; + + // wait until socket becomes writable again + _handler->monitor(_connection, _socket, readable | writable); + + // allow chaining + return monitor.valid() ? this : nullptr; + + case SSL_ERROR_NONE: + // we're ready for the next instruction from userspace + _state = state_idle; + + // turns out no error occured, an no action has to be rescheduled + _handler->monitor(_connection, _socket, _out || _closed ? readable | writable : readable); + + // allow chaining + return monitor.valid() ? this : nullptr; + + default: + // if we have already reported an error to user space, we can go to the final state right away + if (_finalized) return finalstate(monitor); + + // remember that we've sent out an error + _finalized = true; + + // tell the handler + _handler->onError(_connection, "ssl error"); + + // go to the final state + return finalstate(monitor); + } + } + + /** + * Parse the received buffer + * @param monitor object to check the existance of the connection object + * @param size number of bytes available + * @return TcpState + */ + TcpState *parse(const Monitor &monitor, size_t size) + { + // we need a local copy of the buffer - because it is possible that "this" + // object gets destructed halfway through the call to the parse() method + TcpInBuffer buffer(std::move(_in)); + + // parse the buffer + auto processed = _connection->parse(buffer); + + // "this" could be removed by now, check this + if (!monitor.valid()) return nullptr; + + // shrink buffer + buffer.shrink(processed); + + // restore the buffer as member + _in = std::move(buffer); + + // do we have to reallocate? + if (!_reallocate) return this; + + // reallocate the buffer + _in.reallocate(_reallocate); + + // we can remove the reallocate instruction + _reallocate = 0; + + // done + return this; + } + + /** + * Perform a write operation + * @param monitor object to check the existance of the connection object + * @param readable is the connection also readable and should we call a read operation afterwards? + * @return TcpState* + */ + TcpState *write(const Monitor &monitor, bool readable) + { + // assume default state + _state = state_idle; + + // because the output buffer contains a lot of small buffers, we can do multiple + // operations till the buffer is empty (but only if the socket is not also + // readable, because then we want to read that data first instead of endless writes + do + { + // try to send more data from the outgoing buffer + auto result = _out.sendto(_ssl); + + // we may have to repeat the operation on failure + if (result > 0) continue; + + // the operation failed, we may have to repeat our call + return repeat(monitor, state_sending, OpenSSL::SSL_get_error(_ssl, result)); + } + while (_out && !readable); + + // proceed with the read operation or the event loop + return readable ? receive(monitor, false) : proceed(); + } + + /** + * Perform a receive operation + * @param monitor object to check the existance of the connection object + * @param writable is the socket writable, and should we start a write operation after this operation? + * @return TcpState + */ + TcpState *receive(const Monitor &monitor, bool writable) + { + // start a loop + do + { + // assume default state + _state = state_idle; + + // read data from ssl into the buffer + auto result = _in.receivefrom(_ssl, _connection->expected()); + + // if this is a failure, we are going to repeat the operation + if (result <= 0) return repeat(monitor, state_receiving, OpenSSL::SSL_get_error(_ssl, result)); + + // go process the received data + auto *nextstate = parse(monitor, result); + + // leap out if we moved to a different state + if (nextstate != this) return nextstate; + } + while (OpenSSL::SSL_pending(_ssl) > 0); + + // proceed with the write operation or the event loop + return writable && _out ? write(monitor, false) : proceed(); + } + + +public: + /** + * Constructor + * @param connection Parent TCP connection object + * @param socket The socket filedescriptor + * @param ssl The SSL structure + * @param buffer The buffer that was already built + * @param handler User-supplied handler object + */ + SslConnected(TcpConnection *connection, int socket, SslWrapper &&ssl, TcpOutBuffer &&buffer, TcpHandler *handler) : + TcpState(connection, handler), + _ssl(std::move(ssl)), + _socket(socket), + _out(std::move(buffer)), + _in(4096), + _state(_out ? state_sending : state_idle) + { + // tell the handler to monitor the socket if there is an out + _handler->monitor(_connection, _socket, _state == state_sending ? readable | writable : readable); + } + + /** + * Destructor + */ + virtual ~SslConnected() noexcept + { + // close the socket + close(); + } + + /** + * The filedescriptor of this connection + * @return int + */ + virtual int fileno() const override { return _socket; } + + /** + * Number of bytes in the outgoing buffer + * @return std::size_t + */ + virtual std::size_t queued() const override { return _out.size(); } + + /** + * Process the filedescriptor in the object + * @param monitor Object that can be used to find out if connection object is still alive + * @param fd The filedescriptor that is active + * @param flags AMQP::readable and/or AMQP::writable + * @return New implementation object + */ + virtual TcpState *process(const Monitor &monitor, int fd, int flags) override + { + // the socket must be the one this connection writes to + if (fd != _socket) return this; + + // if we were busy with a write operation, we have to repeat that + if (_state == state_sending) return write(monitor, flags & readable); + + // same is true for read operations, they should also be repeated + if (_state == state_receiving) return receive(monitor, flags & writable); + + // if the socket is readable, we are going to receive data + if (flags & readable) return receive(monitor, flags & writable); + + // socket is not readable (so it must be writable), do we have data to write? + if (_out) return write(monitor, false); + + // the only scenario in which we can end up here is the socket should be + // closed, but instead of moving to the shutdown-state right, we call proceed() + // because that function is a little more careful + return proceed(); + } + + /** + * Flush the connection, sent all buffered data to the socket + * @param monitor Object to check if connection still exists + * @return TcpState new tcp state + */ + virtual TcpState *flush(const Monitor &monitor) override + { + // we are not going to do this is object is busy reading + if (_state == state_receiving) return this; + + // create an object to wait for the filedescriptor to becomes active + Wait wait(_socket); + + // keep looping while we have an outgoing buffer + while (_out) + { + // move to the idle-state + _state = state_idle; + + // try to send more data from the outgoing buffer + auto result = _out.sendto(_ssl); + + // was this a success? + if (result > 0) + { + // proceed to the next state + auto *nextstate = proceed(); + + // leap out if we move to a different state + if (nextstate != this) return nextstate; + } + else + { + // error was returned, so we must investigate what is going on + auto error = OpenSSL::SSL_get_error(_ssl, result); + + // get the next state given the error + auto *nextstate = repeat(monitor, state_sending, error); + + // leap out if we move to a different state + if (nextstate != this) return nextstate; + + // check the type of error, and wait now + switch (error) { + case SSL_ERROR_WANT_READ: wait.readable(); break; + case SSL_ERROR_WANT_WRITE: wait.active(); break; + } + } + } + + // done + return this; + } + + /** + * Send data over the connection + * @param buffer buffer to send + * @param size size of the buffer + */ + virtual void send(const char *buffer, size_t size) override + { + // put the data in the outgoing buffer + _out.add(buffer, size); + + // if we're already busy with sending or receiving, we first have to wait + // for that operation to complete before we can move on + if (_state != state_idle) return; + + // let's wait until the socket becomes writable + _handler->monitor(_connection, _socket, readable | writable); + } + + /** + * Report that heartbeat negotiation is going on + * @param heartbeat suggested heartbeat + * @return uint16_t accepted heartbeat + */ + virtual uint16_t reportNegotiate(uint16_t heartbeat) override + { + // remember that we have to reallocate (_in member can not be accessed because it is moved away) + _reallocate = _connection->maxFrame(); + + // pass to base + return TcpState::reportNegotiate(heartbeat); + } + + /** + * Report a connection error + * @param error + */ + virtual void reportError(const char *error) override + { + // we want to start the elegant ssl shutdown procedure, so we call reportClosed() here too, + // because that function does exactly what we want to do here too + reportClosed(); + + // if the user was already notified of an final state, we do not have to proceed + if (_finalized) return; + + // remember that this is the final call to user space + _finalized = true; + + // pass to handler + _handler->onError(_connection, error); + } + + /** + * Report to the handler that the connection was nicely closed + */ + virtual void reportClosed() override + { + // remember that the object is going to be closed + _closed = true; + + // if the previous operation is still in progress we can wait for that + if (_state != state_idle) return; + + // wait until the connection is writable so that we can close it then + _handler->monitor(_connection, _socket, readable | writable); + } +}; + +/** + * End of namespace + */ +} diff --git a/src/linux_tcp/sslcontext.h b/src/linux_tcp/sslcontext.h new file mode 100644 index 0000000..ab9dbc7 --- /dev/null +++ b/src/linux_tcp/sslcontext.h @@ -0,0 +1,71 @@ +/** + * SslContext.h + * + * Class to create and maintain a tcp ssl context + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * Class definition + */ +class SslContext +{ +private: + /** + * The wrapped context + * @var SSL_CTX + */ + SSL_CTX *_ctx; + +public: + /** + * Constructor + * @param method the connect method to use + * @throws std::runtime_error + */ + SslContext(const SSL_METHOD *method) : _ctx(OpenSSL::SSL_CTX_new(method)) + { + // report error + if (_ctx == nullptr) throw std::runtime_error("failed to construct ssl context"); + } + + /** + * Copy constructor is delete because the object is refcounted, + * and we do not have a decent way to update the refcount in openssl 1.0 + * @param that + */ + SslContext(SslContext &that) = delete; + + /** + * Destructor + */ + virtual ~SslContext() + { + // free resource (this updates the refcount -1, and may destruct it) + OpenSSL::SSL_CTX_free(_ctx); + } + + /** + * Cast to the actual context + * @return SSL_CTX * + */ + operator SSL_CTX * () { return _ctx; } +}; + +/** + * End of namespace + */ +} + diff --git a/src/linux_tcp/sslhandshake.h b/src/linux_tcp/sslhandshake.h new file mode 100644 index 0000000..064d181 --- /dev/null +++ b/src/linux_tcp/sslhandshake.h @@ -0,0 +1,262 @@ +/** + * SslHandshake.h + * + * Implementation of the TCP state that is responsible for setting + * up the STARTTLS handshake. + * + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Dependencies + */ +#include "tcpoutbuffer.h" +#include "sslconnected.h" +#include "wait.h" +#include "sslwrapper.h" +#include "sslcontext.h" + +/** + * Set up namespace + */ +namespace AMQP { + +/** + * Class definition + */ +class SslHandshake : public TcpState, private Watchable +{ +private: + /** + * SSL structure + * @var SslWrapper + */ + SslWrapper _ssl; + + /** + * The socket file descriptor + * @var int + */ + int _socket; + + /** + * The outgoing buffer + * @var TcpOutBuffer + */ + TcpOutBuffer _out; + + + /** + * Report a new state + * @param monitor + * @return TcpState + */ + TcpState *nextstate(const Monitor &monitor) + { + // check if the handler allows the connection + bool allowed = _handler->onSecured(_connection, _ssl); + + // leap out if the user space function destructed the object + if (!monitor.valid()) return nullptr; + + // copy the socket because we might forget it + auto socket = _socket; + + // forget the socket member to prevent that it is closed by the destructor + _socket = -1; + + // if connection is allowed, we move to the next state + if (allowed) return new SslConnected(_connection, socket, std::move(_ssl), std::move(_out), _handler); + + // report that the connection is broken + _handler->onError(_connection, "TLS connection has been rejected"); + + // the onError method could have destructed this object + if (!monitor.valid()) return nullptr; + + // shutdown the connection + return new SslShutdown(_connection, socket, std::move(_ssl), true, _handler); + } + + /** + * Helper method to report an error + * @param monitor + * @return TcpState* + */ + TcpState *reportError(const Monitor &monitor) + { + // we are no longer interested in any events for this socket + _handler->monitor(_connection, _socket, 0); + + // close the socket + close(_socket); + + // forget filedescriptor + _socket = -1; + + // we have an error - report this to the user + _handler->onError(_connection, "failed to setup ssl connection"); + + // done, go to the closed state (plus check if connection still exists, because + // after the onError() call the user space program may have destructed that object) + return monitor.valid() ? new TcpClosed(this) : nullptr; + } + + /** + * Proceed with the handshake + * @param events the events to wait for on the socket + * @return TcpState + */ + TcpState *proceed(int events) + { + // tell the handler that we want to listen for certain events + _handler->monitor(_connection, _socket, events); + + // allow chaining + return this; + } + +public: + /** + * Constructor + * + * @todo catch the exception! + * + * @param connection Parent TCP connection object + * @param socket The socket filedescriptor + * @param hostname The hostname to connect to + * @param context SSL context + * @param buffer The buffer that was already built + * @param handler User-supplied handler object + * @throws std::runtime_error + */ + SslHandshake(TcpConnection *connection, int socket, const std::string &hostname, TcpOutBuffer &&buffer, TcpHandler *handler) : + TcpState(connection, handler), + _ssl(SslContext(OpenSSL::TLS_client_method())), + _socket(socket), + _out(std::move(buffer)) + { + // we will be using the ssl context as a client + OpenSSL::SSL_set_connect_state(_ssl); + + // associate domain name with the connection + OpenSSL::SSL_ctrl(_ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, (void *)hostname.data()); + + // associate the ssl context with the socket filedescriptor + if (OpenSSL::SSL_set_fd(_ssl, socket) == 0) throw std::runtime_error("failed to associate filedescriptor with ssl socket"); + + // we are going to wait until the socket becomes writable before we start the handshake + _handler->monitor(_connection, _socket, writable); + } + + /** + * Destructor + */ + virtual ~SslHandshake() noexcept + { + // leap out if socket is invalidated + if (_socket < 0) return; + + // the object got destructed without moving to a new state, this is normally + close(_socket); + } + + /** + * The filedescriptor of this connection + * @return int + */ + virtual int fileno() const override { return _socket; } + + /** + * Number of bytes in the outgoing buffer + * @return std::size_t + */ + virtual std::size_t queued() const override { return _out.size(); } + + /** + * Process the filedescriptor in the object + * @param monitor Object to check if connection still exists + * @param fd Filedescriptor that is active + * @param flags AMQP::readable and/or AMQP::writable + * @return New state object + */ + virtual TcpState *process(const Monitor &monitor, int fd, int flags) override + { + // must be the socket + if (fd != _socket) return this; + + // start the ssl handshake + int result = OpenSSL::SSL_do_handshake(_ssl); + + // if the connection succeeds, we can move to the ssl-connected state + if (result == 1) return nextstate(monitor); + + // error was returned, so we must investigate what is going on + auto error = OpenSSL::SSL_get_error(_ssl, result); + + // check the error + switch (error) { + case SSL_ERROR_WANT_READ: return proceed(readable); + case SSL_ERROR_WANT_WRITE: return proceed(readable | writable); + default: return reportError(monitor); + } + } + + /** + * Send data over the connection + * @param buffer buffer to send + * @param size size of the buffer + */ + virtual void send(const char *buffer, size_t size) override + { + // the handshake is still busy, outgoing data must be cached + _out.add(buffer, size); + } + + /** + * Flush the connection, sent all buffered data to the socket + * @param monitor Object to check if connection still exists + * @return TcpState new tcp state + */ + virtual TcpState *flush(const Monitor &monitor) override + { + // create an object to wait for the filedescriptor to becomes active + Wait wait(_socket); + + // keep looping + while (true) + { + // start the ssl handshake + int result = OpenSSL::SSL_do_handshake(_ssl); + + // if the connection succeeds, we can move to the ssl-connected state + if (result == 1) return nextstate(monitor); + + // error was returned, so we must investigate what is going on + auto error = OpenSSL::SSL_get_error(_ssl, result); + + // check the error + switch (error) { + + // if openssl reports that socket readability or writability is needed, + // we wait for that until this situation is reached + case SSL_ERROR_WANT_READ: wait.readable(); break; + case SSL_ERROR_WANT_WRITE: wait.active(); break; + + // something is wrong, we proceed to the next state + default: return reportError(monitor); + } + } + } +}; + +/** + * End of namespace + */ +} + diff --git a/src/linux_tcp/sslshutdown.h b/src/linux_tcp/sslshutdown.h new file mode 100644 index 0000000..64791cf --- /dev/null +++ b/src/linux_tcp/sslshutdown.h @@ -0,0 +1,246 @@ +/** + * SslShutdown.h + * + * Class that takes care of the final handshake to close a SSL connection + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * Class definition + */ +class SslShutdown : public TcpState, private Watchable +{ +private: + /** + * The SSL context + * @var SslWrapper + */ + SslWrapper _ssl; + + /** + * Socket file descriptor + * @var int + */ + int _socket; + + /** + * Have we already notified user space of connection end? + * @var bool + */ + bool _finalized; + + + /** + * Close the socket + * @return bool + */ + bool close() + { + // skip if already closed + if (_socket < 0) return false; + + // we're no longer interested in events + _handler->monitor(_connection, _socket, 0); + + // close the socket + ::close(_socket); + + // forget the socket + _socket = -1; + + // done + return true; + } + + /** + * Report an error + * @param monitor object to check if connection still exists + * @return TcpState* + */ + TcpState *reporterror(const Monitor &monitor) + { + // close the socket + close(); + + // if we have already told user space that connection is gone + if (_finalized) return new TcpClosed(this); + + // object will be finalized now + _finalized = true; + + // inform user space that the party is over + _handler->onError(_connection, "ssl shutdown error"); + + // go to the final state (if not yet disconnected) + return monitor.valid() ? new TcpClosed(this) : nullptr; + } + + /** + * Proceed with the next operation after the previous operation was + * a success, possibly changing the filedescriptor-monitor + * @param monitor object to check if connection still exists + * @return TcpState* + */ + TcpState *proceed(const Monitor &monitor) + { + // close the socket + close(); + + // if we have already told user space that connection is gone + if (_finalized) return new TcpClosed(this); + + // object will be finalized now + _finalized = true; + + // inform user space that the party is over + _handler->onClosed(_connection); + + // go to the final state (if not yet disconnected) + return monitor.valid() ? new TcpClosed(this) : nullptr; + } + + /** + * Method to repeat the previous call + * @param monitor object to check if connection still exists + * @param result result of an earlier openssl operation + * @return TcpState* + */ + TcpState *repeat(const Monitor &monitor, int result) + { + // error was returned, so we must investigate what is going on + auto error = OpenSSL::SSL_get_error(_ssl, result); + + // check the error + switch (error) { + case SSL_ERROR_WANT_READ: + // the operation must be repeated when readable + _handler->monitor(_connection, _socket, readable); + return this; + + case SSL_ERROR_WANT_WRITE: + // wait until socket becomes writable again + _handler->monitor(_connection, _socket, readable | writable); + return this; + + default: + // go to the final state (if not yet disconnected) + return reporterror(monitor); + } + } + + +public: + /** + * Constructor + * @param connection Parent TCP connection object + * @param socket The socket filedescriptor + * @param ssl The SSL structure + * @param finalized Is the user already notified of connection end (onError() has been called) + * @param handler User-supplied handler object + */ + SslShutdown(TcpConnection *connection, int socket, SslWrapper &&ssl, bool finalized, TcpHandler *handler) : + TcpState(connection, handler), + _ssl(std::move(ssl)), + _socket(socket), + _finalized(finalized) + { + // wait until the socket is accessible + _handler->monitor(_connection, _socket, readable | writable); + } + + /** + * Destructor + */ + virtual ~SslShutdown() noexcept + { + // close the socket + close(); + } + + /** + * The filedescriptor of this connection + * @return int + */ + virtual int fileno() const override { return _socket; } + + /** + * Process the filedescriptor in the object + * @param monitor Object to check if connection still exists + * @param fd The filedescriptor that is active + * @param flags AMQP::readable and/or AMQP::writable + * @return New implementation object + */ + virtual TcpState *process(const Monitor &monitor, int fd, int flags) override + { + // the socket must be the one this connection writes to + if (fd != _socket) return this; + + // close the connection + auto result = OpenSSL::SSL_shutdown(_ssl); + + // on result==0 we need an additional call + while (result == 0) result = OpenSSL::SSL_shutdown(_ssl); + + // if this is a success, we can proceed with the event loop + if (result > 0) return proceed(monitor); + + // the operation failed, we may have to repeat our call + else return repeat(monitor, result); + } + + /** + * Flush the connection, sent all buffered data to the socket + * @param monitor Object to check if connection still exists + * @return TcpState new tcp state + */ + virtual TcpState *flush(const Monitor &monitor) override + { + // create an object to wait for the filedescriptor to becomes active + Wait wait(_socket); + + // keep looping + while (true) + { + // close the connection + auto result = OpenSSL::SSL_shutdown(_ssl); + + // on result==0 we need an additional call + while (result == 0) result = OpenSSL::SSL_shutdown(_ssl); + + // if this is a success, we can proceed with the event loop + if (result > 0) return proceed(monitor); + + // error was returned, so we must investigate what is going on + auto error = OpenSSL::SSL_get_error(_ssl, result); + + // check the error + switch (error) { + + // if openssl reports that socket readability or writability is needed, + // we wait for that until this situation is reached + case SSL_ERROR_WANT_READ: wait.readable(); break; + case SSL_ERROR_WANT_WRITE: wait.active(); break; + + // something is wrong, we proceed to the next state + default: return reporterror(monitor); + } + } + } +}; + +/** + * End of namespace + */ +} diff --git a/src/linux_tcp/sslwrapper.h b/src/linux_tcp/sslwrapper.h new file mode 100644 index 0000000..b72e861 --- /dev/null +++ b/src/linux_tcp/sslwrapper.h @@ -0,0 +1,86 @@ +/** + * SslWrapper.h + * + * Wrapper around a SSL pointer + * + * @author Emiel Bruijntjes + * @copyright 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * Class definition + */ +class SslWrapper +{ +private: + /** + * The wrapped object + * @var SSL* + */ + SSL *_ssl; + +public: + /** + * Constructor + * @param ctx + * @param file + */ + SslWrapper(SSL_CTX *ctx) : _ssl(OpenSSL::SSL_new(ctx)) + { + // report error + if (_ssl == nullptr) throw std::runtime_error("failed to construct ssl structure"); + + //OpenSSL::SSL_use_certificate_file(_ssl, "cert.pem", SSL_FILETYPE_PEM); + } + + /** + * Copy constructor is removed because openssl 1.0 has no way to up refcount + * (otherwise we could be safely copying objects around) + * @param that + */ + SslWrapper(const SslWrapper &that) = delete; + + /** + * Move constructor + * @param that + */ + SslWrapper(SslWrapper &&that) : _ssl(that._ssl) + { + // invalidate other object + that._ssl = nullptr; + } + + /** + * Destructor + */ + virtual ~SslWrapper() + { + // do nothing if already moved away + if (_ssl == nullptr) return; + + // destruct object + OpenSSL::SSL_free(_ssl); + } + + /** + * Cast to the SSL* + * @return SSL * + */ + operator SSL * () const { return _ssl; } +}; + +/** + * End of namespace + */ +} + diff --git a/src/tcpclosed.h b/src/linux_tcp/tcpclosed.h similarity index 100% rename from src/tcpclosed.h rename to src/linux_tcp/tcpclosed.h diff --git a/src/tcpconnected.h b/src/linux_tcp/tcpconnected.h similarity index 67% rename from src/tcpconnected.h rename to src/linux_tcp/tcpconnected.h index 85bc377..34b6c47 100644 --- a/src/tcpconnected.h +++ b/src/linux_tcp/tcpconnected.h @@ -5,7 +5,7 @@ * the hostname was resolved into an IP address * * @author Emiel Bruijntjes - * @copyright 2015 - 2016 Copernica BV + * @copyright 2015 - 2018 Copernica BV */ /** @@ -18,6 +18,7 @@ */ #include "tcpoutbuffer.h" #include "tcpinbuffer.h" +#include "wait.h" /** * Set up namespace @@ -53,8 +54,36 @@ private: * @var size_t */ size_t _reallocate = 0; + + /** + * Have we already made the last report to the user (about an error or closed connection?) + * @var bool + */ + bool _finalized = false; + /** + * Close the connection + * @return bool + */ + bool close() + { + // do nothing if already closed + if (_socket < 0) return false; + + // and stop monitoring it + _handler->monitor(_connection, _socket, 0); + + // close the socket + ::close(_socket); + + // forget filedescriptor + _socket = -1; + + // done + return true; + } + /** * Helper method to report an error * @return bool Was an error reported? @@ -64,6 +93,16 @@ private: // some errors are ok and do not (necessarily) mean that we're disconnected if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return false; + // connection can be closed now + close(); + + // if the user has already been notified, we do not have to do anything else + if (_finalized) return true; + + // update the _finalized member before we make the call to user space because + // the user space may destruct this object + _finalized = true; + // we have an error - report this to the user _handler->onError(_connection, strerror(errno)); @@ -83,30 +122,6 @@ private: return monitor.valid() ? new TcpClosed(this) : nullptr; } - /** - * Wait until the socket is writable - * @return bool - */ - bool wait4writable() - { - // we need the fd-sets - fd_set readables, writables, exceptions; - - // initialize all the sets - FD_ZERO(&readables); - FD_ZERO(&writables); - FD_ZERO(&exceptions); - - // add the one socket - FD_SET(_socket, &writables); - - // wait for the socket - auto result = select(_socket + 1, &readables, &writables, &exceptions, nullptr); - - // check for success - return result == 0; - } - public: /** * Constructor @@ -133,30 +148,34 @@ public: */ virtual ~TcpConnected() noexcept { - // skip if handler is already forgotten - if (_handler == nullptr) return; - - // we no longer have to monitor the socket - _handler->monitor(_connection, _socket, 0); - // close the socket - close(_socket); + close(); } - + + /** + * The filedescriptor of this connection + * @return int + */ + virtual int fileno() const override { return _socket; } + + /** + * Number of bytes in the outgoing buffer + * @return std::size_t + */ + virtual std::size_t queued() const override { return _out.size(); } + /** * Process the filedescriptor in the object + * @param monitor Monitor to check if the object is still alive * @param fd Filedescriptor that is active * @param flags AMQP::readable and/or AMQP::writable * @return New state object */ - virtual TcpState *process(int fd, int flags) override + virtual TcpState *process(const Monitor &monitor, int fd, int flags) override { // must be the socket if (fd != _socket) return this; - // because the object might soon be destructed, we create a monitor to check this - Monitor monitor(this); - // can we write more data to the socket? if (flags & writable) { @@ -164,7 +183,7 @@ public: auto result = _out.sendto(_socket); // are we in an error state? - if (result < 0 && reportError()) return nextState(monitor); + if (result < 0 && reportError()) return nextState(monitor); // if buffer is empty by now, we no longer have to check for // writability, but only for readability @@ -197,7 +216,10 @@ public: _in = std::move(buffer); // do we have to reallocate? - if (_reallocate) { _in.reallocate(_reallocate); _reallocate = 0; } + if (_reallocate) _in.reallocate(_reallocate); + + // we can remove the reallocate instruction + _reallocate = 0; } // keep same object @@ -232,18 +254,22 @@ public: /** * Flush the connection, sent all buffered data to the socket + * @param monitor Object to check if connection still lives * @return TcpState new tcp state */ - virtual TcpState *flush() override + virtual TcpState *flush(const Monitor &monitor) override { - // keep running until the out buffer is empty + // create an object to wait for the filedescriptor to becomes active + Wait wait(_socket); + + // keep running until the out buffer is not empty while (_out) { // poll the socket, is it already writable? - if (!wait4writable()) return this; + if (!wait.writable()) return this; // socket is writable, send as much data as possible - auto *newstate = process(_socket, writable); + auto *newstate = process(monitor, _socket, writable); // are we done if (newstate != this) return newstate; @@ -260,35 +286,54 @@ public: */ virtual uint16_t reportNegotiate(uint16_t heartbeat) override { - // remember that we have to reallocated (_in member can not be accessed because it is moved away) + // remember that we have to reallocate (_in member can not be accessed because it is moved away) _reallocate = _connection->maxFrame(); // pass to base return TcpState::reportNegotiate(heartbeat); } + /** + * Report to the handler that the object is in an error state. + * @param error + */ + virtual void reportError(const char *error) override + { + // close the socket + close(); + + // if the user was already notified of an final state, we do not have to proceed + if (_finalized) return; + + // remember that this is the final call to user space + _finalized = true; + + // pass to handler + _handler->onError(_connection, error); + } + /** * Report to the handler that the connection was nicely closed + * This is the counter-part of the connection->close() call. */ virtual void reportClosed() override { - // we no longer have to monitor the socket - _handler->monitor(_connection, _socket, 0); + // we will shutdown the socket in a very elegant way, we notify the peer + // that we will not be sending out more write operations + shutdown(_socket, SHUT_WR); + + // we still monitor the socket for readability to see if our close call was + // confirmed by the peer + _handler->monitor(_connection, _socket, readable); - // close the socket - close(_socket); + // if the user was already notified of an final state, we do not have to proceed + if (_finalized) return; - // socket is closed now - _socket = -1; + // remember that this is the final call to user space + _finalized = true; - // copy the handler (if might destruct this object) - auto *handler = _handler; - - // reset member before the handler can make a mess of it - _handler = nullptr; - - // notify to handler - handler->onClosed(_connection); + // pass to handler + _handler->onClosed(_connection); } }; diff --git a/src/tcpconnection.cpp b/src/linux_tcp/tcpconnection.cpp similarity index 83% rename from src/tcpconnection.cpp rename to src/linux_tcp/tcpconnection.cpp index cf31fe7..b8f0cd1 100644 --- a/src/tcpconnection.cpp +++ b/src/linux_tcp/tcpconnection.cpp @@ -4,7 +4,7 @@ * Implementation file for the TCP connection * * @author Emiel Bruijntjes - * @copyright 2015 - 2016 Copernica BV + * @copyright 2015 - 2018 Copernica BV */ /** @@ -12,6 +12,7 @@ */ #include "includes.h" #include "tcpresolver.h" +#include "tcpstate.h" /** * Set up namespace @@ -24,9 +25,33 @@ namespace AMQP { * @param hostname The address to connect to */ TcpConnection::TcpConnection(TcpHandler *handler, const Address &address) : - _state(new TcpResolver(this, address.hostname(), address.port(), handler)), + _state(new TcpResolver(this, address.hostname(), address.port(), address.secure(), handler)), _connection(this, address.login(), address.vhost()) {} +/** + * Destructor + */ +TcpConnection::~TcpConnection() noexcept = default; + +/** + * The filedescriptor that is used for this connection + * @return int + */ +int TcpConnection::fileno() const +{ + // pass on to the state object + return _state->fileno(); +} + +/** + * The number of outgoing bytes queued on this connection. + * @return std::size_t + */ +std::size_t TcpConnection::queued() const +{ + return _state->queued(); +} + /** * Process the TCP connection * This method should be called when the filedescriptor that is registered @@ -38,11 +63,11 @@ TcpConnection::TcpConnection(TcpHandler *handler, const Address &address) : */ void TcpConnection::process(int fd, int flags) { - // monitor the object for destruction - Monitor monitor{ this }; + // monitor the object for destruction, because you never know what the user + Monitor monitor(this); // pass on the the state, that returns a new impl - auto *result = _state->process(fd, flags); + auto *result = _state->process(monitor, fd, flags); // are we still valid if (!monitor.valid()) return; @@ -68,7 +93,7 @@ void TcpConnection::flush() while (true) { // flush the object - auto *newstate = _state->flush(); + auto *newstate = _state->flush(monitor); // done if object no longer exists if (!monitor.valid()) return; @@ -122,14 +147,8 @@ void TcpConnection::onHeartbeat(Connection *connection) */ void TcpConnection::onError(Connection *connection, const char *message) { - // current object is going to be removed, but we have to keep it for a while - auto ptr = std::move(_state); - - // object is now in a closed state - _state.reset(new TcpClosed(ptr.get())); - // tell the implementation to report the error - ptr->reportError(message); + _state->reportError(message); } /** @@ -148,14 +167,8 @@ void TcpConnection::onConnected(Connection *connection) */ void TcpConnection::onClosed(Connection *connection) { - // current object is going to be removed, but we have to keep it for a while - auto ptr = std::move(_state); - - // object is now in a closed state - _state.reset(new TcpClosed(ptr.get())); - // tell the implementation to report that connection is closed now - ptr->reportClosed(); + _state->reportClosed(); } /** diff --git a/src/tcpinbuffer.h b/src/linux_tcp/tcpinbuffer.h similarity index 80% rename from src/tcpinbuffer.h rename to src/linux_tcp/tcpinbuffer.h index 65971ec..79b708f 100644 --- a/src/tcpinbuffer.h +++ b/src/linux_tcp/tcpinbuffer.h @@ -12,6 +12,11 @@ */ #pragma once +/** + * Dependencies + */ + #include + /** * Beginnig of namespace */ @@ -107,6 +112,27 @@ public: // done return result; } + + /** + * Receive data from a socket + * @param ssl ssl wrapped socket to read from + * @param expected number of bytes that the library expects + * @return ssize_t + */ + ssize_t receivefrom(SSL *ssl, uint32_t expected) + { + // number of bytes to that still fit in the buffer + size_t bytes = expected - _size; + + // read data + auto result = OpenSSL::SSL_read(ssl, (void *)(_data + _size), bytes); + + // update total buffer size on success + if (result > 0) _size += result; + + // done + return result; + } /** * Shrink the buffer (in practice this is always called with the full buffer size) diff --git a/src/tcpoutbuffer.h b/src/linux_tcp/tcpoutbuffer.h similarity index 72% rename from src/tcpoutbuffer.h rename to src/linux_tcp/tcpoutbuffer.h index 186a9b4..31dbd27 100644 --- a/src/tcpoutbuffer.h +++ b/src/linux_tcp/tcpoutbuffer.h @@ -5,7 +5,7 @@ * output buffer. This is the implementation of that buffer * * @author Emiel Bruijntjes - * @copyright 2015 - 2016 Copernica BV + * @copyright 2015 - 2018 Copernica BV */ /** @@ -18,7 +18,8 @@ */ #include #include - +#include "openssl.h" + /** * FIONREAD on Solaris is defined elsewhere */ @@ -195,10 +196,36 @@ public: } } + /** + * Fill an iovec buffer + * @param buffers the buffers to be filled + * @param count number of buffers available + * @return size_t the number of buffers that were filled + */ + size_t fill(struct iovec buffers[], size_t count) const + { + // index counter + size_t index = 0; + + // iterate over the buffers + for (const auto &str : _buffers) + { + // fill buffer + buffers[index].iov_base = (void *)(index == 0 ? str.data() + _skip : str.data()); + buffers[index].iov_len = index == 0 ? str.size() - _skip : str.size(); + + // update counter for next iteration + if (++index >= count) return count; + } + + // done + return index; + } + /** * Send the buffer to a socket - * @param socket - * @return ssize_t + * @param socket the socket to send data to + * @return ssize_t number of bytes sent (or the same result as sendmsg() in case of an error) */ ssize_t sendto(int socket) { @@ -211,20 +238,6 @@ public: // we're going to fill a lot of buffers (64 should normally be enough) struct iovec buffer[64]; - // index counter - size_t index = 0; - - // iterate over the buffers - for (const auto &str : _buffers) - { - // fill buffer - buffer[index].iov_base = (void *)(index == 0 ? str.data() + _skip : str.data()); - buffer[index].iov_len = index == 0 ? str.size() - _skip : str.size(); - - // update counter for next iteration - if (++index >= 64) break; - } - // create the message header struct msghdr header; @@ -233,7 +246,10 @@ public: // save the buffers in the message header header.msg_iov = buffer; - header.msg_iovlen = index; + header.msg_iovlen = fill(buffer, 64); + + // do nothing if no buffers were filled + if (header.msg_iovlen == 0) break; // send the data auto result = sendmsg(socket, &header, AMQP_CPP_MSG_NOSIGNAL); @@ -251,6 +267,32 @@ public: // done return total; } + + /** + * Send the buffer to an SSL connection + * @param ssl the ssl context to send data to + * @return ssize_t number of bytes sent, or the return value of ssl_write + */ + ssize_t sendto(SSL *ssl) + { + // we're going to fill a lot of buffers (for ssl only one buffer at a time can be sent) + struct iovec buffer[1]; + + // fill the buffers, and leap out if there is no data + auto buffers = fill(buffer, 1); + + // just to be sure we do this check + if (buffers == 0) return 0; + + // send the data + auto result = OpenSSL::SSL_write(ssl, buffer[0].iov_base, buffer[0].iov_len); + + // on success we shrink the buffer + if (result > 0) shrink(result); + + // done + return result; + } }; /** diff --git a/src/tcpresolver.h b/src/linux_tcp/tcpresolver.h similarity index 73% rename from src/tcpresolver.h rename to src/linux_tcp/tcpresolver.h index de543b4..5fc002a 100644 --- a/src/tcpresolver.h +++ b/src/linux_tcp/tcpresolver.h @@ -5,7 +5,7 @@ * server, and to make the initial connection * * @author Emiel Bruijntjes - * @copyright 2015 - 2016 Copernica BV + * @copyright 2015 - 2018 Copernica BV */ /** @@ -20,6 +20,8 @@ #include "tcpstate.h" #include "tcpclosed.h" #include "tcpconnected.h" +#include "openssl.h" +#include "sslhandshake.h" #include /** @@ -39,6 +41,12 @@ private: */ std::string _hostname; + /** + * Should we be using a secure connection? + * @var bool + */ + bool _secure; + /** * The portnumber to connect to * @var uint16_t @@ -84,6 +92,9 @@ private: // prevent exceptions try { + // check if we support openssl in the first place + if (_secure && !OpenSSL::valid()) throw std::runtime_error("Secure connection cannot be established: libssl.so cannot be loaded"); + // get address info AddressInfo addresses(_hostname.data(), _port); @@ -142,11 +153,13 @@ public: * @param connection Parent connection object * @param hostname The hostname for the lookup * @param portnumber The portnumber for the lookup + * @param secure Do we need a secure tls connection when ready? * @param handler User implemented handler object */ - TcpResolver(TcpConnection *connection, const std::string &hostname, uint16_t port, TcpHandler *handler) : + TcpResolver(TcpConnection *connection, const std::string &hostname, uint16_t port, bool secure, TcpHandler *handler) : TcpState(connection, handler), _hostname(hostname), + _secure(secure), _port(port) { // tell the event loop to monitor the filedescriptor of the pipe @@ -171,44 +184,66 @@ public: _thread.join(); } + /** + * Number of bytes in the outgoing buffer + * @return std::size_t + */ + virtual std::size_t queued() const override { return _buffer.size(); } + + /** + * Proceed to the next state + * @return TcpState * + */ + TcpState *proceed() + { + // do we have a valid socket? + if (_socket >= 0) + { + // if we need a secure connection, we move to the tls handshake + // @todo catch possible exception + if (_secure) return new SslHandshake(_connection, _socket, _hostname, std::move(_buffer), _handler); + + // otherwise we have a valid regular tcp connection + return new TcpConnected(_connection, _socket, std::move(_buffer), _handler); + } + else + { + // report error + _handler->onError(_connection, _error.data()); + + // create dummy implementation + return new TcpClosed(_connection, _handler); + } + } + /** * Wait for the resolver to be ready + * @param monitor Object to check if connection still exists * @param fd The filedescriptor that is active * @param flags Flags to indicate that fd is readable and/or writable * @return New implementation object */ - virtual TcpState *process(int fd, int flags) override + virtual TcpState *process(const Monitor &monitor, int fd, int flags) override { // only works if the incoming pipe is readable if (fd != _pipe.in() || !(flags & readable)) return this; - // do we have a valid socket? - if (_socket >= 0) return new TcpConnected(_connection, _socket, std::move(_buffer), _handler); - - // report error - _handler->onError(_connection, _error.data()); - - // create dummy implementation - return new TcpClosed(_connection, _handler); + // proceed to the next state + return proceed(); } /** * Flush state / wait for the connection to complete + * @param monitor Object to check if connection still exists * @return New implementation object */ - virtual TcpState *flush() override + virtual TcpState *flush(const Monitor &monitor) override { // just wait for the other thread to be ready _thread.join(); - // do we have a valid socket? - if (_socket >= 0) return new TcpConnected(_connection, _socket, std::move(_buffer), _handler); - - // report error - _handler->onError(_connection, _error.data()); - - // create dummy implementation - return new TcpClosed(_connection, _handler); + // proceed to the next state + return proceed(); } /** diff --git a/src/linux_tcp/tcpstate.h b/src/linux_tcp/tcpstate.h new file mode 100644 index 0000000..c7660c9 --- /dev/null +++ b/src/linux_tcp/tcpstate.h @@ -0,0 +1,183 @@ +/** + * TcpState.h + * + * Base class / interface of the various states of the TCP connection + * + * @author Emiel Bruijntjes + * @copyright 2015 - 2018 Copernica BV + */ + +/** + * Include guard + */ +#pragma once + +/** + * Set up namespace + */ +namespace AMQP { + +/** + * Class definition + */ +class TcpState +{ +protected: + /** + * Parent TcpConnection object as is seen by the user + * @var TcpConnection + */ + TcpConnection *_connection; + + /** + * User-supplied handler + * @var TcpHandler + */ + TcpHandler *_handler; + +protected: + /** + * Protected constructor + * @param connection Original TCP connection object + * @param handler User-supplied handler class + */ + TcpState(TcpConnection *connection, TcpHandler *handler) : + _connection(connection), _handler(handler) {} + + /** + * Protected "copy" constructor + * @param state Original TcpState object + */ + TcpState(const TcpState *state) : + _connection(state->_connection), _handler(state->_handler) {} + +public: + /** + * Virtual destructor + */ + virtual ~TcpState() = default; + + /** + * The filedescriptor of this connection + * @return int + */ + virtual int fileno() const { return -1; } + + /** + * The number of outgoing bytes queued on this connection. + * @return size_t + */ + virtual std::size_t queued() const { return 0; } + + /** + * Process the filedescriptor in the object + * + * This method should return the handler object that will be responsible for + * all future readable/writable events for the file descriptor, or nullptr + * if the underlying connection object has already been destructed by the + * user and it would be pointless to set up a new handler. + * + * @param monitor Monitor that can be used to check if the tcp connection is still alive + * @param fd The filedescriptor that is active + * @param flags AMQP::readable and/or AMQP::writable + * @return New implementation object + */ + virtual TcpState *process(const Monitor &monitor, int fd, int flags) + { + // default implementation does nothing and preserves same implementation + return this; + } + + /** + * Send data over the connection + * @param buffer Buffer to send + * @param size Size of the buffer + */ + virtual void send(const char *buffer, size_t size) + { + // default does nothing + } + + /** + * Flush the connection, all outgoing operations should be completed. + * + * If the state changes during the operation, the new state object should + * be returned instead, or nullptr if the user has closed the connection + * in the meantime. If the connection object got destructed by a user space + * call, this method should return nullptr. A monitor object is pass in to + * allow the flush() method to check if the connection still exists. + * + * If this object returns a new state object (instead of "this"), the + * connection object will immediately proceed with calling flush() on that + * new state object too. + * + * @param monitor Monitor that can be used to check if the tcp connection is still alive + * @return TcpState New implementation object + */ + virtual TcpState *flush(const Monitor &monitor) { return this; } + + /** + * Report to the handler that heartbeat negotiation is going on + * @param heartbeat suggested heartbeat + * @return uint16_t accepted heartbeat + */ + virtual uint16_t reportNegotiate(uint16_t heartbeat) + { + // pass to handler + return _handler->onNegotiate(_connection, heartbeat); + } + + /** + * Report to the handler that the object is in an error state. + * + * This is the last method to be called on the handler object, from now on + * the handler will no longer be called to report things to user space. + * The state object itself stays active, and further calls to process() + * may be possible. + * + * @param error + */ + virtual void reportError(const char *error) + { + // pass to handler + _handler->onError(_connection, error); + } + + /** + * Report that a heartbeat frame was received + */ + virtual void reportHeartbeat() + { + // pass to handler + _handler->onHeartbeat(_connection); + } + + /** + * Report to the handler that the connection is ready for use + */ + virtual void reportConnected() + { + // pass to handler + _handler->onConnected(_connection); + } + + /** + * Report to the handler that the connection was correctly closed, after + * the user has called the Connection::close() method. The underlying TCP + * connection still has to be closed. + * + * This is the last method that is called on the object, from now on no + * other methods may be called on the _handler variable. + */ + virtual void reportClosed() + { + // pass to handler + _handler->onClosed(_connection); + } +}; + +/** + * End of namespace + */ +} + diff --git a/src/linux_tcp/wait.h b/src/linux_tcp/wait.h new file mode 100644 index 0000000..5db5242 --- /dev/null +++ b/src/linux_tcp/wait.h @@ -0,0 +1,97 @@ +/** + * Wait.h + * + * Class to wait for a socket to become readable and/or writable + * + * @copyright 2018 Copernica BV + */ + + /** + * Include guard + */ +#pragma once + +/** + * Begin of namespace + */ +namespace AMQP { + +/** + * Class definition + */ +class Wait +{ +private: + /** + * Set with just one filedescriptor + * @var fd_set + */ + fd_set _set; + + /** + * The current socket // @todo what is it exactly? + * @var int + */ + int _socket; + +public: + /** + * Constructor + * @param fd the filedescriptor that we're waiting on + */ + Wait(int fd) : _socket(fd) + { + // initialize the set + FD_ZERO(&_set); + + // add the one socket + FD_SET(_socket, &_set); + } + + /** + * No copying + * @param that + */ + Wait(const Wait &that) = delete; + + /** + * Destructor + */ + virtual ~Wait() = default; + + /** + * Wait until the filedescriptor becomes readable + * @return bool + */ + bool readable() + { + // wait for the socket + return select(_socket + 1, &_set, nullptr, nullptr, nullptr) > 0; + } + + /** + * Wait until the filedescriptor becomes writable + * @return bool + */ + bool writable() + { + // wait for the socket + return select(_socket + 1, nullptr, &_set, nullptr, nullptr) > 0; + } + + /** + * Wait until a filedescriptor becomes active (readable or writable) + * @return bool + */ + bool active() + { + // wait for the socket + return select(_socket + 1, &_set, &_set, nullptr, nullptr) > 0; + } +}; + +/** + * End of namespace + */ +} + diff --git a/src/passthroughbuffer.h b/src/passthroughbuffer.h index 87509f3..4126eca 100644 --- a/src/passthroughbuffer.h +++ b/src/passthroughbuffer.h @@ -17,7 +17,7 @@ */ #include #include -#include "../include/frame.h" +#include "amqpcpp/frame.h" /** * Set up namespace diff --git a/src/queuebindframe.h b/src/queuebindframe.h index bc4d2e9..8418fe1 100644 --- a/src/queuebindframe.h +++ b/src/queuebindframe.h @@ -87,7 +87,7 @@ public: * @param Table arguments additional arguments */ QueueBindFrame(uint16_t channel, const std::string& name, const std::string& exchange, const std::string& routingKey = "", bool noWait = false, const Table& arguments = {}) : - QueueFrame(channel, (name.length() + exchange.length() + routingKey.length() + arguments.size() + 6) ), // 3 extra per string, 1 for bools, 2 for deprecated field + QueueFrame(channel, (uint32_t)(name.length() + exchange.length() + routingKey.length() + arguments.size() + 6) ), // 3 extra per string, 1 for bools, 2 for deprecated field _name(name), _exchange(exchange), _routingKey(routingKey), diff --git a/src/queuedeclareframe.h b/src/queuedeclareframe.h index 475c8b6..627b17c 100644 --- a/src/queuedeclareframe.h +++ b/src/queuedeclareframe.h @@ -80,7 +80,7 @@ public: * @param Table arguments additional arguments, implementation dependent */ QueueDeclareFrame(uint16_t channel, const std::string& name = "", bool passive = false, bool durable = false, bool exclusive = false, bool autoDelete = false, bool noWait = false, const Table& arguments = {}) : - QueueFrame(channel, (name.length() + arguments.size() + 4 ) ), // 1 extra for string size, 1 for bools, 2 for deprecated value + QueueFrame(channel, (uint32_t)(name.length() + arguments.size() + 4 ) ), // 1 extra for string size, 1 for bools, 2 for deprecated value _name(name), _bools(passive, durable, exclusive, autoDelete, noWait), _arguments(arguments) diff --git a/src/queuedeclareokframe.h b/src/queuedeclareokframe.h index 44821a5..99ad1e8 100644 --- a/src/queuedeclareokframe.h +++ b/src/queuedeclareokframe.h @@ -62,7 +62,7 @@ public: * @param failingMethod failing method id if applicable */ QueueDeclareOKFrame(uint16_t channel, const std::string& name, int32_t messageCount, int32_t consumerCount) : - QueueFrame(channel, name.length() + 9), // 4 per int, 1 for string size + QueueFrame(channel, (uint32_t)(name.length() + 9)), // 4 per int, 1 for string size _name(name), _messageCount(messageCount), _consumerCount(consumerCount) diff --git a/src/queuedeleteframe.h b/src/queuedeleteframe.h index dc9a00a..f76e736 100644 --- a/src/queuedeleteframe.h +++ b/src/queuedeleteframe.h @@ -69,7 +69,7 @@ public: * @param noWait do not wait on response */ QueueDeleteFrame(uint16_t channel, const std::string& name, bool ifUnused = false, bool ifEmpty = false, bool noWait = false) : - QueueFrame(channel, name.length() + 4), // 1 for string length, 1 for bools, 2 for deprecated field + QueueFrame(channel, (uint32_t)(name.length() + 4)), // 1 for string length, 1 for bools, 2 for deprecated field _name(name), _bools(ifUnused, ifEmpty, noWait) {} diff --git a/src/queuepurgeframe.h b/src/queuepurgeframe.h index 08cfad5..016cfd0 100644 --- a/src/queuepurgeframe.h +++ b/src/queuepurgeframe.h @@ -65,7 +65,7 @@ public: * @return newly created Queuepurgeframe */ QueuePurgeFrame(uint16_t channel, const std::string& name, bool noWait = false) : - QueueFrame(channel, name.length() + 4), // 1 extra for string length, 1 for bool, 2 for deprecated field + QueueFrame(channel, (uint32_t)(name.length() + 4)), // 1 extra for string length, 1 for bool, 2 for deprecated field _name(name), _noWait(noWait) {} diff --git a/src/queueunbindframe.h b/src/queueunbindframe.h index 8460663..e40b3b9 100644 --- a/src/queueunbindframe.h +++ b/src/queueunbindframe.h @@ -80,7 +80,7 @@ public: * @param arguments additional arguments, implementation dependant. */ QueueUnbindFrame(uint16_t channel, const std::string& name, const std::string& exchange, const std::string& routingKey = "", const Table& arguments = {} ) : - QueueFrame(channel, (name.length() + exchange.length() + routingKey.length() + arguments.size() + 5) ), // 1 per string, 2 for deprecated field + QueueFrame(channel, (uint32_t)(name.length() + exchange.length() + routingKey.length() + arguments.size() + 5) ), // 1 per string, 2 for deprecated field _name(name), _exchange(exchange), _routingKey(routingKey), diff --git a/src/table.cpp b/src/table.cpp index ac2fd45..d4177f4 100644 --- a/src/table.cpp +++ b/src/table.cpp @@ -21,7 +21,7 @@ Table::Table(ReceivedFrame &frame) ShortString name(frame); // subtract number of bytes to read, plus one byte for the decoded type - bytesToRead -= (name.size() + 1); + bytesToRead -= (uint32_t)(name.size() + 1); // get the field Field *field = Field::decode(frame); @@ -31,7 +31,7 @@ Table::Table(ReceivedFrame &frame) _fields[name] = std::shared_ptr(field); // subtract size - bytesToRead -= field->size(); + bytesToRead -= (uint32_t)field->size(); } } diff --git a/src/tcpstate.h b/src/tcpstate.h deleted file mode 100644 index bdedcf2..0000000 --- a/src/tcpstate.h +++ /dev/null @@ -1,141 +0,0 @@ -/** - * TcpState.h - * - * Base class / interface of the various states of the TCP connection - * - * @author Emiel Bruijntjes - * @copyright 2015 - 2016 Copernica BV - */ - -/** - * Include guard - */ -#pragma once - -/** - * Set up namespace - */ -namespace AMQP { - -/** - * Class definition - */ -class TcpState -{ -protected: - /** - * Parent TcpConnection object as is seen by the user - * @var TcpConnection - */ - TcpConnection *_connection; - - /** - * User-supplied handler - * @var TcpHandler - */ - TcpHandler *_handler; - -protected: - /** - * Protected constructor - * @param connection Original TCP connection object - * @param handler User-supplied handler class - */ - TcpState(TcpConnection *connection, TcpHandler *handler) : - _connection(connection), _handler(handler) {} - - /** - * Protected "copy" constructor - * @param state Original TcpState object - */ - TcpState(const TcpState *state) : - _connection(state->_connection), _handler(state->_handler) {} - -public: - /** - * Virtual destructor - */ - virtual ~TcpState() = default; - - /** - * Process the filedescriptor in the object - * @param fd The filedescriptor that is active - * @param flags AMQP::readable and/or AMQP::writable - * @return New implementation object - */ - virtual TcpState *process(int fd, int flags) - { - // default implementation does nothing and preserves same implementation - return this; - } - - /** - * Send data over the connection - * @param buffer buffer to send - * @param size size of the buffer - */ - virtual void send(const char *buffer, size_t size) - { - // default does nothing - } - - /** - * Report that heartbeat negotiation is going on - * @param heartbeat suggested heartbeat - * @return uint16_t accepted heartbeat - */ - virtual uint16_t reportNegotiate(uint16_t heartbeat) - { - // pass to handler - return _handler->onNegotiate(_connection, heartbeat); - } - - /** - * Flush the connection - * @return TcpState new implementation object - */ - virtual TcpState *flush() { return this; } - - /** - * Report to the handler that the object is in an error state - * @param error - */ - void reportError(const char *error) - { - // pass to handler - _handler->onError(_connection, error); - } - - /** - * Report that a heartbeat frame was received - */ - void reportHeartbeat() - { - // pass to handler - _handler->onHeartbeat(_connection); - } - - /** - * Report to the handler that the connection is ready for use - */ - void reportConnected() - { - // pass to handler - _handler->onConnected(_connection); - } - - /** - * Report to the handler that the connection was nicely closed - */ - virtual void reportClosed() - { - // pass to handler - _handler->onClosed(_connection); - } -}; - -/** - * End of namespace - */ -} - diff --git a/tests/Makefile b/tests/Makefile index b5bf2f3..ee7f59c 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,6 +1,6 @@ -CPP = g++ +CPP = g++ CPPFLAGS = -Wall -c -I. -O2 -flto -std=c++11 -g -LD = g++ +LD = g++ LDFLAGS = -lamqpcpp -lcopernica_event -lcopernica_network -lev RESULT = a.out SOURCES = $(wildcard *.cpp) diff --git a/tests/address.cpp b/tests/address.cpp index 2145931..0de9a15 100644 --- a/tests/address.cpp +++ b/tests/address.cpp @@ -18,6 +18,7 @@ * @param argv * @return int */ +/* int main(int argc, const char *argv[]) { // iterate over the arguments @@ -37,3 +38,4 @@ int main(int argc, const char *argv[]) // done return 0; } +*/ \ No newline at end of file diff --git a/tests/libev.cpp b/tests/libev.cpp deleted file mode 100644 index 1bb610b..0000000 --- a/tests/libev.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/** - * LibEV.cpp - * - * Test program to check AMQP functionality based on LibEV - * - * @author Emiel Bruijntjes - * @copyright 2015 - 2016 Copernica BV - */ - -/** - * Dependencies - */ -#include -#include -#include - -/** - * Main program - * @return int - */ -int main() -{ - // access to the event loop - auto *loop = EV_DEFAULT; - - // handler for libev - AMQP::LibEvHandler handler(loop); - - // make a connection - AMQP::TcpConnection connection(&handler, AMQP::Address("amqp://guest:guest@localhost/")); - - // we need a channel too - AMQP::TcpChannel channel(&connection); - - // create a temporary queue - channel.declareQueue(AMQP::exclusive).onSuccess([&connection](const std::string &name, uint32_t messagecount, uint32_t consumercount) { - - // report the name of the temporary queue - std::cout << "declared queue " << name << std::endl; - - // now we can close the connection - connection.close(); - }); - - // run the loop - ev_run(loop, 0); - - // done - return 0; -} - diff --git a/tests/myconnection.cpp b/tests/myconnection.cpp index 96f3580..8875d02 100644 --- a/tests/myconnection.cpp +++ b/tests/myconnection.cpp @@ -30,7 +30,7 @@ MyConnection::MyConnection(const std::string &ip) : _socket(Event::MainLoop::instance(), this) { // start connecting - if (_socket.connect(Network::Ipv4Address(ip), 5672)) return; + if (_socket.connect(Dns::IpAddress(ip), 5672)) return; // failure onFailure(&_socket); @@ -96,21 +96,30 @@ void MyConnection::onConnected(Network::TcpSocket *socket) std::cout << "queue declared" << std::endl; // start consuming - _channel->consume("my_queue").onReceived([](const AMQP::Message &message, uint64_t deliveryTag, bool redelivered) { - std::cout << "received: " << message.message() << std::endl; + _channel->consume("my_queue").onReceived([this](const AMQP::Message &message, uint64_t deliveryTag, bool redelivered) { + std::cout << "consumed from exchange " << message.exchange() << " " << message.routingkey() << ": " << std::string(message.body(), message.bodySize()) << std::endl; + _channel->ack(deliveryTag); }); }); // declare an exchange - _channel->declareExchange().onSuccess([]() { + _channel->declareExchange("my_exchange", AMQP::direct).onSuccess([]() { std::cout << "exchange declared" << std::endl; }); // bind queue and exchange _channel->bindQueue("my_exchange", "my_queue", "key").onSuccess([this]() { std::cout << "queue bound to exchange" << std::endl; + + // callback for returns + auto callback = [](const AMQP::Message &message, int16_t code, const std::string &description) { - _channel->publish("my_exchange", "key", "just a message"); + std::cout << "message was returned: " << code << " " << description << ": " << std::string(message.body(), message.bodySize()) << std::endl; + + }; + + _channel->publish("my_exchange", "key", "just a message", AMQP::mandatory).onReturned(callback); + _channel->publish("my_exchange", "unknown key", "just another message", AMQP::mandatory).onReturned(callback); }); } @@ -156,7 +165,7 @@ void MyConnection::onData(Network::TcpSocket *socket, Network::Buffer *buffer) if (!_connection) return; // let the data be handled by the connection - size_t bytes = _connection->parse(buffer->data(), buffer->size()); + size_t bytes = _connection->parse(buffer->buffer(), buffer->size()); // shrink the buffer buffer->shrink(bytes);