diff --git a/CMakeLists.txt b/CMakeLists.txt index e5eda3d8..9ff15317 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,8 @@ set_target_properties(mapcache PROPERTIES SOVERSION 1 ) +set_property(TARGET mapcache PROPERTY C_STANDARD 11) + # Add compiler flags for warnings if(CMAKE_COMPILER_IS_GNUCC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror=declaration-after-statement") @@ -278,6 +280,17 @@ if(WITH_RIAK) endif(RIAK_FOUND) endif (WITH_RIAK) +if(WITH_SWIFT) + find_package(JSONC) + if(JSONC_FOUND) + include_directories(${JSONC_INCLUDE_DIRS}) + target_link_libraries(mapcache ${JSONC_LIBRARIES}) + set (USE_SWIFT 1) + else(JSONC_FOUND) + report_optional_not_found(JSONC) + endif(JSONC_FOUND) +endif (WITH_SWIFT) + if(UNIX) target_link_libraries(mapcache ${CMAKE_DL_LIBS} m ) endif(UNIX) @@ -328,6 +341,7 @@ status_optional_component("PCRE" "${USE_PCRE}" "${PCRE_LIBRARY}") status_optional_component("Experimental mapserver support" "${USE_MAPSERVER}" "${MAPSERVER_LIBRARY}") status_optional_component("RIAK" "${USE_RIAK}" "${RIAK_LIBRARY}") status_optional_component("GDAL" "${USE_GDAL}" "${GDAL_LIBRARY}") +status_optional_component("JSONC" "${USE_SWIFT}" "${JSONC_LIBRARIES}") message(STATUS " * Optional features") status_optional_feature("MAPCACHE_DETAIL" "${WITH_MAPCACHE_DETAIL}") diff --git a/cmake/FindJSONC.cmake b/cmake/FindJSONC.cmake new file mode 100644 index 00000000..30e223d5 --- /dev/null +++ b/cmake/FindJSONC.cmake @@ -0,0 +1,31 @@ +# Try to find json-c +# Once done, this will define +# +# JSONC_FOUND - system has jsonc +# JSONC__INCLUDE_DIRS - the jsonc include directories +# JSONC__LIBRARIES - jsonc libraries directories + +if(JSONC_INCLUDE_DIRS AND JSONC_LIBRARIES) +set(JSONC_FIND_QUIETLY TRUE) +endif(JSONC_INCLUDE_DIRS AND JSONC_LIBRARIES) + +find_path(JSONC_INCLUDE_DIR json.h + HINTS + /usr/include/json-c/ + /usr/local/include/json-c/ + ) +find_library(JSONC_LIBRARY json-c + HINTS + /usr/lib/ + /usr/local/lib + ) + +set(JSONC_INCLUDE_DIRS ${JSONC_INCLUDE_DIR}) +set(JSONC_LIBRARIES ${JSONC_LIBRARY}) + +# handle the QUIETLY and REQUIRED arguments and set JSONC_FOUND to TRUE if +# all listed variables are TRUE +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(jsonc DEFAULT_MSG JSONC_INCLUDE_DIRS JSONC_LIBRARIES) + +mark_as_advanced(JSONC_INCLUDE_DIRS JSONC_LIBRARIES) \ No newline at end of file diff --git a/include/keystone-client.h b/include/keystone-client.h new file mode 100644 index 00000000..62a637e8 --- /dev/null +++ b/include/keystone-client.h @@ -0,0 +1,218 @@ +/* + * Based on https://github.com/ukyg9e5r6k7gubiekd6/keystone-client + */ + +#ifndef KEYSTONE_CLIENT_H_ +#define KEYSTONE_CLIENT_H_ + +#include +#include +#include + +/** + * Keystone version to be used. + * Currently supported are V1 and V3. + */ +enum keystone_auth_version { + KS_AUTH_V1 = 1, + KS_AUTH_V3 = 3 +}; + +/** + * High-level types of errors which can occur while attempting to use Keystone. + * More detail is available from lower-level libraries (such as curl and libjson) + * using error callbacks specific to those libraries. + */ +enum keystone_error { + KSERR_SUCCESS = 0, /* Success */ + KSERR_INIT_FAILED = 1, /* Initialisation of this library failed */ + KSERR_INVARG = 2, /* Invalid argument */ + KSERR_ALLOC_FAILED = 3, /* Memory allocation failed */ + KSERR_URL_FAILED = 4, /* Network operation on a URL failed */ + KSERR_AUTH_REJECTED = 5, /* Authentication attempt rejected */ + KSERR_NOTFOUND = 6, /* Requested service(s) not found in service catalog */ + KSERR_PARSE = 7 /* Failed to parse Keystone response */ +}; + +/** + * Types of OpenStack service. + */ +enum openstack_service { + OS_SERVICE_KEYSTONE = 0, /* Keystone authentication and service catalog */ + OS_SERVICE_NOVA = 1, /* Nova compute */ + OS_SERVICE_NOVA_EC2 = 2, /* Nova compute, EC2 API */ + OS_SERVICE_SWIFT = 3, /* Swift object storage */ + OS_SERVICE_SWIFT_S3 = 4, /* Swift object storage, S3 API */ + OS_SERVICE_CINDER = 5, /* Cinder block storage */ + OS_SERVICE_GLANCE = 6 /* Glance image storage */ +}; +#define OS_SERVICE_MAX OS_SERVICE_GLANCE + +/** + * Types of OpenStack service endpoint. + */ +enum openstack_service_endpoint_url_type { + OS_ENDPOINT_URL_PUBLIC = 0, /* Service's public endpoint */ + OS_ENDPOINT_URL_PRIVATE = 1, /* Service's private endpoint */ + OS_ENDPOINT_URL_INTERNAL = 2, /* Service's internal endpoint */ +}; +#define OS_ENDPOINT_URL_MAX OS_ENDPOINT_URL_INTERNAL + +/* keystone client library's per-thread private context */ +struct keystone_context_private { + CURL *curl; /* Handle to curl library's easy interface */ + unsigned int debug; + struct json_tokener *json_tokeniser; /* libjson0 library's JSON tokeniser */ + struct json_object *services; /* service catalog JSON array */ + enum keystone_auth_version version; + unsigned int verify_cert_trusted; /* True if the peer's certificate must chain to a trusted CA, false otherwise */ + unsigned int verify_cert_hostname; /* True if the peer's certificate's hostname must be correct, false otherwise */ + char *auth_payload; /* Authentication POST payload, containing credentials */ + char *auth_token; /* Authentication token previously obtained from Keystone */ +}; + +typedef struct keystone_context_private keystone_context_private_t; + +/* A function which allocates, re-allocates or de-allocates memory */ +typedef void *(*keystone_allocator_func_t)(void *ptr, size_t newsize); + +/* A function which receives curl errors */ +typedef void (*curl_error_callback_t)(const char *curl_funcname, CURLcode res); + +/* A function which receives libjson errors */ +typedef void (*json_error_callback_t)(const char *json_funcname, enum json_tokener_error json_err); + +/* A function which receives Keystone errors */ +typedef void (*keystone_error_callback_t)(const char *keystone_operation, enum keystone_error keystone_err); + +/** + * All use of this library is performed within a 'context'. + * Contexts cannot be shared among threads; each thread must have its own context. + * Your program is responsible for allocating and freeing context structures. + * Contexts should be zeroed out prior to use. + */ +struct keystone_context { + + /* These members are 'public'; your program can (and should) set them at will */ + + /** + * Called when a libcurl error occurs. + * Your program may set this function pointer in order to perform custom error handling. + * If this is NULL at the time keystone_start is called, a default handler will be used. + */ + curl_error_callback_t curl_error; + /** + * Called when a libjson error occurs. + * Your program may set this function in order to perform custom error handling. + * If this is NULL at the time keystone_start is called, a default handler will be used. + */ + json_error_callback_t json_error; + /** + * Called when a Keystone error occurs. + * Your program may set this function in order to perform custom error handling. + * If this is NULL at the time keystone_start is called, a default handler will be used. + */ + keystone_error_callback_t keystone_error; + /** + * Called when this library needs to allocate, re-allocate or free memory. + * If size is zero and ptr is NULL, nothing is done. + * If size is zero and ptr is non-NULL, the previously-allocated memory at ptr is to be freed. + * If size is non-zero and ptr is NULL, memory of the given size is to be allocated. + * If size is non-zero and ptr is non-NULL, the previously-allocated memory at ptr + * is to be re-allocated to be the given size. + * If this function pointer is NULL at the time keystone_start is called, a default re-allocator will be used. + */ + keystone_allocator_func_t allocator; + /* This member (and its members, recursively) are 'private'. */ + /* They should not be modified by your program unless you *really* know what you're doing. */ + keystone_context_private_t pvt; +}; + +typedef struct keystone_context keystone_context_t; + +/** + * Begin using this library. + * The context passed must be zeroed out, except for the public part, + * in which you may want to over-ride the function pointers. + * Function pointers left NULL will be given meaningful defaults. + * This must be called early in the execution of your program, + * before additional threads (if any) are created. + * This must be called before any other use of this library by your program. + * These restrictions are imposed by libcurl, and the libcurl restrictions are in turn + * imposed by the libraries that libcurl uses. + * If your program is a library, it will need to expose a similar API to, + * and expose similar restrictions on, its users. + */ +enum keystone_error keystone_global_init(void); + +/** + * Cease using this library. + * This must be called late in the execution of your program, + * after all secondary threads (if any) have exited, + * so that there is precisely one thread in your program at the time of the call. + * This library must not be used by your program after this function is called. + * This function must be called exactly once for each successful prior call to keystone_global_init + * by your program. + * These restrictions are imposed by libcurl, and the libcurl restrictions are in turn + * imposed by the libraries that libcurl uses. + * If your program is a library, it will need to expose a similar API to, + * and expose similar restrictions on, its users. + */ +void keystone_global_cleanup(void); + +/** + * Begin using this library for a single thread of your program. + * This must be called by each thread of your program in order to use this library. + */ +enum keystone_error keystone_start(keystone_context_t *context); + +/** + * Cease using this library for a single thread. + * This must be called by each thread of your program after it is finished using this library. + * Each thread in your program must call this function precisely once for each successful prior call + * to keystone_start by that thread. + * After this call, the context is invalid. + */ +void keystone_end(keystone_context_t *context); + +/** + * Control whether a proxy (eg HTTP or SOCKS) is used to access the Keystone server. + * Argument must be a URL, or NULL if no proxy is to be used. + */ +enum keystone_error keystone_set_proxy(keystone_context_t *context, const char *proxy_url); + +/** + * Control verbose logging to stderr of the actions of this library and the libraries it uses. + * Currently this enables logging to standard error of libcurl's actions. + */ +enum keystone_error keystone_set_debug(keystone_context_t *context, unsigned int enable_debugging); + +const char *service_name(unsigned int service); +const char *endpoint_url_name(unsigned int endpoint); + +/** + * Set the auth version to be used. Default is v1. + */ +enum keystone_error keystone_set_auth_version(keystone_context_t *context, enum keystone_auth_version version); + +/** + * Authenticate against a Keystone authentication service with the given tenant and user names and password. + * This yields an authorisation token, which is then used to access all Swift services. + */ +enum keystone_error keystone_authenticate(keystone_context_t *context, const char *url, const char *tenant_name, const char *username, const char *password); + +/** + * Return the previously-acquired Keystone authentication token, if any. + * If no authentication token has previously been acquired, return NULL. + */ +const char *keystone_get_auth_token(keystone_context_t *context); + +/** + * Given a desired service type and version and type of URL, find a service of the given type in Keystone's catalog of services, + * then find an endpoint of that service with the given API version, then return its URL of the given type. + * Return NULL if the service cannot be found, or if no endpoint of the given version can be found, + * or if the service endpoint of the given version has no URL of the given type. + */ +const char *keystone_get_service_url(keystone_context_t *context, enum openstack_service desired_service_type, unsigned int desired_api_version, enum openstack_service_endpoint_url_type endpoint_url_type); + +#endif /* KEYSTONE_CLIENT_H_ */ diff --git a/include/mapcache-config.h.in b/include/mapcache-config.h.in index 40e8062e..a141d030 100644 --- a/include/mapcache-config.h.in +++ b/include/mapcache-config.h.in @@ -14,6 +14,7 @@ #cmakedefine USE_MAPSERVER 1 #cmakedefine USE_RIAK 1 #cmakedefine USE_GDAL 1 +#cmakedefine USE_SWIFT 1 #cmakedefine HAVE_STRNCASECMP 1 #cmakedefine HAVE_SYMLINK 1 diff --git a/include/mapcache.h b/include/mapcache.h index f52f245a..adb72686 100644 --- a/include/mapcache.h +++ b/include/mapcache.h @@ -325,6 +325,7 @@ typedef enum { ,MAPCACHE_CACHE_COMPOSITE ,MAPCACHE_CACHE_COUCHBASE ,MAPCACHE_CACHE_RIAK + ,MAPCACHE_CACHE_SWIFT } mapcache_cache_type; /** \interface mapcache_cache @@ -406,6 +407,11 @@ mapcache_cache* mapcache_cache_tc_create(mapcache_context *ctx); */ mapcache_cache* mapcache_cache_riak_create(mapcache_context *ctx); +/** + * \memberof mapcache_cache_swift + */ +mapcache_cache* mapcache_cache_swift_create(mapcache_context *ctx); + /** @} */ diff --git a/include/swift-client.h b/include/swift-client.h new file mode 100644 index 00000000..d9561644 --- /dev/null +++ b/include/swift-client.h @@ -0,0 +1,282 @@ +/* + * Based on https://github.com/ukyg9e5r6k7gubiekd6/swift-client + */ + +#ifndef SWIFT_CLIENT_H_ +#define SWIFT_CLIENT_H_ + +#include +#include +#include + +/** + * High-level types of errors which can occur while attempting to use Swift. + * More detail is available from lower-level libraries (such as curl) + * using error callbacks specific to those libraries. + */ +enum swift_error { + SCERR_SUCCESS = 0, /* Success */ + SCERR_INIT_FAILED = 1, /* Initialisation of this library failed */ + SCERR_INVARG = 2, /* An invalid argument was supplied */ + SCERR_ALLOC_FAILED = 3, /* Memory allocation failed */ + SCERR_URL_FAILED = 4, /* Network operation on a URL failed */ + SCERR_FILEIO_FAILED = 5, /* I/O operation on a file failed */ + SCERR_AUTH_FAILED = 6, /* Authentication failure */ + SCERR_NOT_FOUND = 7, /* The resource was not found */ + SCERR_INVALID_REQ = 8, /* An invalid request was sent */ + SCERR_SERVER_ERROR = 9 /* The server errored */ +}; + +/* Operations supported by Swift */ +enum swift_operation { + CREATE_CONTAINER = 0, + LIST_CONTAINER = 1, + SET_CONTAINER_METADATA = 2, + DELETE_CONTAINER = 3, + PUT_OBJECT = 4, + GET_OBJECT = 5, + SET_OBJECT_METADATA = 6, + DELETE_OBJECT = 7, + HAS_OBJECT = 8 +}; + +/* A function which allocates, re-allocates or de-allocates memory */ +typedef void *(*swift_allocator_func_t)(void *ptr, size_t newsize); + +/* A function which receives POSIX errors which set errno */ +typedef void (*errno_callback_t)(const char *funcname, int errno_val); + +/* A function which receives curl errors */ +typedef void (*curl_error_callback_t)(const char *curl_funcname, CURLcode res); + +/* A function which receives libiconv errors */ +// typedef void (*iconv_error_callback_t)(const char *iconv_funcname, int iconv_errno); + +/* A function which supplies data from somewhere of its choice into memory upon demand */ +typedef size_t (*supply_data_func_t)(void *ptr, size_t size, size_t nmemb, void *stream); + +/* A function which receives data into somewhere of its choice from memory upon demand */ +typedef size_t (*receive_data_func_t)(void *ptr, size_t size, size_t nmemb, void *userdata); + +/* swift client library's per-thread private context */ +struct swift_context_private { + CURL *curl; /* Handle to curl library's easy interface */ + // iconv_t iconv; /* iconv library's conversion descriptor */ + unsigned int verify_cert_trusted; /* True if the peer's certificate must chain to a trusted CA, false otherwise */ + unsigned int verify_cert_hostname; /* True if the peer's certificate's hostname must be correct, false otherwise */ + char *container; /* Name of current container */ + char *object; /* Name of current object */ + char *auth_token; /* Authentication token, usually previously obtained from Keystone */ + unsigned int base_url_len; /* Length of Swift base URL, with API version and account, but without container or object */ + char *base_url; /* Swift base URL, with API version and account, but without container or object */ +}; + +typedef struct swift_context_private swift_context_private_t; + +/** + * All use of this library is performed within a 'context'. + * Contexts cannot be shared among threads; each thread must have its own context. + * Your program is responsible for allocating and freeing context structures. + * Contexts should be zeroed out prior to use. + */ +struct swift_context { + + /* These members are 'public'; your program may (and should) set them at will */ + + /** + * Called when an error occurs with a POSIX API which sets 'errno'. + * Your program may set this function pointer in order to perform custom error handling. + * If this is NULL at the time swift_start is called, a default handler will be used. + */ + errno_callback_t errno_error; + /** + * Called when a libcurl error occurs. + * Your program may set this function pointer in order to perform custom error handling. + * If this is NULL at the time swift_start is called, a default handler will be used. + */ + curl_error_callback_t curl_error; + /** + * Called when a libiconv error occurs. + * Your program may set this function in order to perform custom error handling. + * If this is NULL at the time swift_start is called, a default handler will be used. + */ + // iconv_error_callback_t iconv_error; + /** + * Called when this library needs to allocate, re-allocate or free memory. + * If size is zero and ptr is NULL, nothing is done. + * If size is zero and ptr is non-NULL, the previously-allocated memory at ptr is to be freed. + * If size is non-zero and ptr is NULL, memory of the given size is to be allocated. + * If size is non-zero and ptr is non-NULL, the previously-allocated memory at ptr + * is to be re-allocated to be the given size. + * If this function pointer is NULL at the time swift_start is called, a default re-allocator will be used. + */ + swift_allocator_func_t allocator; + /* This member (and its members, recursively) are 'private'. */ + /* They should not be modified by your program unless you *really* know what you're doing. */ + swift_context_private_t pvt; +}; + +typedef struct swift_context swift_context_t; + +/** + * Begin using this library. + * The context passed must be zeroed out, except for the public part, + * in which you may want to over-ride the function pointers. + * Function pointers left NULL will be given meaningful defaults. + * This must be called early in the execution of your program, + * before additional threads (if any) are created. + * This must be called before any other use of this library by your program. + * These restrictions are imposed by libcurl, and the libcurl restrictions are in turn + * imposed by the libraries that libcurl uses. + * If your program is a library, it will need to expose a similar API to, + * and expose similar restrictions on, its users. + */ +enum swift_error swift_global_init(void); + +/** + * Cease using this library. + * This must be called late in the execution of your program, + * after all secondary threads (if any) have exited, + * so that there is precisely one thread in your program at the time of the call. + * This library must not be used by your program after this function is called. + * This function must be called exactly once for each successful prior call to swift_global_init + * by your program. + * These restrictions are imposed by libcurl, and the libcurl restrictions are in turn + * imposed by the libraries that libcurl uses. + * If your program is a library, it will need to expose a similar API to, + * and expose similar restrictions on, its users. + */ +void swift_global_cleanup(void); + +/** + * Begin using this library for a single thread of your program. + * This must be called by each thread of your program in order to use this library. + */ +enum swift_error swift_start(swift_context_t *context); + +/** + * Cease using this library for a single thread. + * This must be called by each thread of your program after it is finished using this library. + * Each thread in your program must call this function precisely once for each successful prior call + * to swift_start by that thread. + * After this call, the context is invalid. + */ +void swift_end(swift_context_t *context); + +/** + * Control verbose logging to stderr of the actions of this library and the libraries it uses. + * Currently this enables logging to standard error of libcurl's actions. + */ +enum swift_error swift_set_debug(swift_context_t *context, unsigned int enable_debugging); + +/** + * Control whether a proxy (eg HTTP or SOCKS) is used to access the Swift server. + * Argument must be a URL, or NULL if no proxy is to be used. + */ +enum swift_error swift_set_proxy(swift_context_t *context, const char *proxy_url); + +/** + * Set the current Swift server URL. This must not contain any path information. + */ +enum swift_error swift_set_url(swift_context_t *context, const char *url); + +/** + * Control whether the Swift server should be accessed via HTTPS, or just HTTP. + */ +enum swift_error swift_set_ssl(swift_context_t *context, unsigned int use_ssl); + +/** + * Control whether an HTTPS server's certificate is required to chain to a trusted CA cert. + */ +enum swift_error swift_verify_cert_trusted(swift_context_t *context, unsigned int require_trusted_cert); + +/** + * Control whether an HTTPS server's hostname is required to match its certificate's hostname. + */ +enum swift_error swift_verify_cert_hostname(swift_context_t *context, unsigned int require_matching_hostname); + +/** + * Set the value of the authentication token to be supplied with requests. + * This should have have been obtained previously from a separate authentication service. + */ +enum swift_error swift_set_auth_token(swift_context_t *context, const char *auth_token); + +/** + * Set the name of the current Swift container. + */ +enum swift_error swift_set_container(swift_context_t *context, char *container_name); + +/** + * Set the name of the current Swift object. + */ +enum swift_error swift_set_object(swift_context_t *context, char *object_name); + +/** + * Check the existence of an object. + */ +enum swift_error +swift_has(swift_context_t *context, int *exists); + +/** + * Retrieve an object from Swift and pass its data to the given callback function. + */ +enum swift_error swift_get(swift_context_t *context, receive_data_func_t receive_data_callback, void *callback_arg); + +/** + * Retrieve an object from Swift and place its data in the given-named file. + */ +enum swift_error swift_get_file(swift_context_t *context, const char *filename); + +/** + * Retrieve an object from Swift and place its data in the variable. + */ +enum swift_error swift_get_data(swift_context_t *context, size_t *size, void **data); + +/** + * Create a Swift container with the current container name. + */ +enum swift_error swift_create_container(swift_context_t *context, size_t metadata_count, const wchar_t **metadata_names, const wchar_t **metadata_values); + +/** + * Delete the Swift container with the current container name. + */ +enum swift_error swift_delete_container(swift_context_t *context); + +/** + * Insert or update an object in Swift using the data supplied by the given callback function. + * The given callback_arg will be passed as the last argument to the callback function. + * Optionally, also attach a set of metadata {name, value} tuples to the object. + * metadata_count specifies the number of {name, value} tuples to be set. This may be zero. + * If metadata_count is non-zero, metadata_names and metadata_values must be arrays, each of length metadata_count, specifying the {name, value} tuples. + */ +enum swift_error swift_put(swift_context_t *context, supply_data_func_t supply_data_callback, void *callback_arg); + +/** + * Insert or update an object in Swift using the data in the given-named file. + * Optionally, also attach a set of metadata {name, value} tuples to the object. + * metadata_count specifies the number of {name, value} tuples to be set. This may be zero. + * If metadata_count is non-zero, metadata_names and metadata_values must be arrays, each of length metadata_count, specifying the {name, value} tuples. + */ +enum swift_error swift_put_file(swift_context_t *context, const char *filename); + +/** + * Insert or update an object in Swift using the size bytes of data located in memory at ptr. + * Optionally, also attach a set of metadata {name, value} tuples to the object. + * metadata_count specifies the number of {name, value} tuples to be set. This may be zero. + * If metadata_count is non-zero, metadata_names and metadata_values must be arrays, each of length metadata_count, specifying the {name, value} tuples. + */ +enum swift_error swift_put_data(swift_context_t *context, const void *ptr, size_t size); + +/** + * Insert or update metadata for the current object. + * metadata_count specifies the number of {name, value} tuples to be set. + * metadata_names and metadata_values must be arrays, each of length metadata_count, specifying the {name, value} tuples. + */ +enum swift_error swift_set_metadata(swift_context_t *context, size_t metadata_count, const wchar_t **metadata_names, const wchar_t **metadata_values); + +/** + * Delete the Swift object with the current container and object names. + */ +enum swift_error swift_delete_object(swift_context_t *context); + +#endif /* SWIFT_CLIENT_H_ */ diff --git a/lib/cache_swift.c b/lib/cache_swift.c new file mode 100644 index 00000000..c86b9429 --- /dev/null +++ b/lib/cache_swift.c @@ -0,0 +1,569 @@ +/****************************************************************************** + * $Id$ + * + * Project: MapServer + * Purpose: MapCache tile caching support file: swift cache backend. + * Author: Fabian Shindler + * + ****************************************************************************** + * Copyright (c) 1996-2019 EOX IT Services GmbH + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies of this Software or works derived from this Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "mapcache.h" +#ifdef USE_SWIFT + +#include "keystone-client.h" +#include "swift-client.h" + +#include +#include +#include + +#include +#include +#include + +typedef struct mapcache_cache_swift mapcache_cache_swift; + +/**\class mapcache_cache_swift + * \brief a mapcache_cache for openstack Swift object storages using keystone authentication + * \implements mapcache_cache + */ +struct mapcache_cache_swift { + mapcache_cache cache; + char *auth_url; + char *tenant; + char *username; + char *password; + char *key_template; + char *container_template; + int debug; + enum keystone_auth_version auth_version; +}; + +struct swift_conn_params { + mapcache_cache_swift *cache; +}; + + +struct swift_connection { + keystone_context_t *keystone_context; + swift_context_t *swift_context; +}; + +void mapcache_swift_authenticate(mapcache_context *ctx, mapcache_cache_swift *cache, struct swift_connection *conn) { + enum keystone_error keystone_err; + enum swift_error swift_err; + char *url; + char *token; + + keystone_err = keystone_authenticate(conn->keystone_context, cache->auth_url, cache->tenant, cache->username, cache->password); + if (keystone_err != KSERR_SUCCESS) { + ctx->set_error(ctx, 500, "failed to keystone_authenticate()"); + return; + } + + token = (char *)keystone_get_auth_token(conn->keystone_context); + if (token == NULL) { + ctx->set_error(ctx, 500, "failed to keystone_get_auth_token()"); + return; + } + + swift_err = swift_set_auth_token(conn->swift_context, token); + if (swift_err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "failed to swift_set_auth_token()"); + return; + } + + // TODO: get version and endpoint type + url = (char *)keystone_get_service_url(conn->keystone_context, OS_SERVICE_SWIFT, 1, OS_ENDPOINT_URL_PRIVATE); + if (token == NULL) { + ctx->set_error(ctx, 500, "failed to keystone_get_service_url()"); + return; + } + + swift_err = swift_set_url(conn->swift_context, url); + if (swift_err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "failed to swift_set_url()"); + return; + } +} + +void mapcache_swift_connection_constructor(mapcache_context *ctx, void **conn_, void *params) { + enum keystone_error keystone_err; + enum swift_error swift_err; + struct swift_connection *conn; + + mapcache_cache_swift *cache = ((struct swift_conn_params*)params)->cache; + + conn = calloc(sizeof(struct swift_connection), 1); + conn->keystone_context = calloc(sizeof(keystone_context_t), 1); + conn->swift_context = calloc(sizeof(swift_context_t), 1); + + keystone_err = keystone_start(conn->keystone_context); + if (keystone_err != KSERR_SUCCESS) { + ctx->set_error(ctx, 500, "failed to keystone_start()"); + return; + } + + keystone_err = keystone_set_auth_version(conn->keystone_context, cache->auth_version); + if (keystone_err != KSERR_SUCCESS) { + keystone_end(conn->keystone_context); + ctx->set_error(ctx, 500, "failed to keystone_set_auth_version()"); + return; + } + + swift_err = swift_start(conn->swift_context); + if (swift_err != SCERR_SUCCESS) { + keystone_end(conn->keystone_context); + ctx->set_error(ctx, 500, "failed to swift_start()"); + return; + } + + if (cache->debug) { + keystone_err = keystone_set_debug(conn->keystone_context, cache->debug); + if (keystone_err != KSERR_SUCCESS) { + keystone_end(conn->keystone_context); + swift_end(conn->swift_context); + ctx->set_error(ctx, 500, "failed to keystone_set_debug()"); + return; + } + swift_err = swift_set_debug(conn->swift_context, cache->debug); + if (swift_err != SCERR_SUCCESS) { + keystone_end(conn->keystone_context); + swift_end(conn->swift_context); + ctx->set_error(ctx, 500, "failed to swift_set_debug()"); + return; + } + } + + mapcache_swift_authenticate(ctx, cache, conn); + if (GC_HAS_ERROR(ctx)) { + keystone_end(conn->keystone_context); + swift_end(conn->swift_context); + return; + } + + *conn_ = conn; +} + +void mapcache_swift_connection_destructor(void *conn_) { + struct swift_connection *conn = (struct swift_connection *)conn_; + + keystone_end(conn->keystone_context); + swift_end(conn->swift_context); + free(conn->keystone_context); + free(conn->swift_context); + free(conn); +} + +static mapcache_pooled_connection* _swift_get_connection(mapcache_context *ctx, mapcache_cache_swift *cache, mapcache_tile *tile) +{ + mapcache_pooled_connection *pc; + struct swift_conn_params params; + + params.cache = cache; + + pc = mapcache_connection_pool_get_connection(ctx,cache->cache.name,mapcache_swift_connection_constructor, + mapcache_swift_connection_destructor, ¶ms); + + return pc; +} + +static int _mapcache_cache_swift_has_tile(mapcache_context *ctx, mapcache_cache *pcache, mapcache_tile *tile) { + mapcache_pooled_connection *pc; + mapcache_cache_swift *cache = (mapcache_cache_swift*) pcache; + struct swift_connection *conn; + enum swift_error err; + int exists; + int rv = MAPCACHE_FALSE; + char *container; + char *key; + + key = mapcache_util_get_tile_key(ctx, tile, cache->key_template, " \r\n\t\f\e\a\b", "#"); + if (GC_HAS_ERROR(ctx)) { + return MAPCACHE_FALSE; + } + + if(strchr(cache->container_template,'{')) { + container = mapcache_util_get_tile_key(ctx, tile, cache->container_template, " \r\n\t\f\e\a\b", "#"); + } else { + container = cache->container_template; + } + + pc = _swift_get_connection(ctx, cache, tile); + + if (GC_HAS_ERROR(ctx)) { + return MAPCACHE_FALSE; + } + conn = pc->connection; + + err = swift_set_container(conn->swift_context, container); + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to set container %s: %d", container, err); + goto cleanup; + } + err = swift_set_object(conn->swift_context, key); + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to set object %s: %d", key, err); + goto cleanup; + } + + err = swift_has(conn->swift_context, &exists); + if (err == SCERR_AUTH_FAILED) { + /* re-authenticate and retry */ + mapcache_swift_authenticate(ctx, cache, conn); + if (GC_HAS_ERROR(ctx)) { + goto cleanup; + } + err = swift_has(conn->swift_context, &exists); + } + + if (err == SCERR_SUCCESS && exists) { + rv = MAPCACHE_TRUE; + } else { + rv = MAPCACHE_FALSE; + } + +cleanup: + mapcache_connection_pool_release_connection(ctx, pc); + return rv; +} + +static void _mapcache_cache_swift_delete(mapcache_context *ctx, mapcache_cache *pcache, mapcache_tile *tile) { + mapcache_pooled_connection *pc; + mapcache_cache_swift *cache = (mapcache_cache_swift*) pcache; + struct swift_connection *conn; + enum swift_error err; + char *container; + char *key; + + key = mapcache_util_get_tile_key(ctx, tile, cache->key_template, " \r\n\t\f\e\a\b", "#"); + if (GC_HAS_ERROR(ctx)) { + return; + } + + if(strchr(cache->container_template,'{')) { + container = mapcache_util_get_tile_key(ctx, tile, cache->container_template, " \r\n\t\f\e\a\b", "#"); + } else { + container = cache->container_template; + } + + pc = _swift_get_connection(ctx, cache, tile); + + if (GC_HAS_ERROR(ctx)) { + return; + } + conn = pc->connection; + + err = swift_set_container(conn->swift_context, container); + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to set container %s: %d", container, err); + goto cleanup; + } + err = swift_set_object(conn->swift_context, key); + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to set object %s: %d", key, err); + goto cleanup; + } + + err = swift_delete_object(conn->swift_context); + if (err == SCERR_AUTH_FAILED) { + mapcache_swift_authenticate(ctx, cache, conn); + if (GC_HAS_ERROR(ctx)) { + goto cleanup; + } + err = swift_delete_object(conn->swift_context); + } else if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to delete object %s: %d", key, err); + } + +cleanup: + mapcache_connection_pool_release_connection(ctx, pc); + return; +} + +/** + * \brief get content of given tile + * + * fills the mapcache_tile::data of the given tile with content stored on the swift object storage + * \private \memberof mapcache_cache_swift + * \sa mapcache_cache::tile_get() + */ +static int _mapcache_cache_swift_get(mapcache_context *ctx, mapcache_cache *pcache, mapcache_tile *tile) { + mapcache_pooled_connection *pc; + mapcache_cache_swift *cache = (mapcache_cache_swift*) pcache; + struct swift_connection *conn; + enum swift_error err; + int rv = MAPCACHE_FAILURE; + char *container; + char *key; + size_t size; + void *data = NULL; + + key = mapcache_util_get_tile_key(ctx, tile, cache->key_template, " \r\n\t\f\e\a\b", "#"); + if (GC_HAS_ERROR(ctx)) { + return MAPCACHE_FAILURE; + } + + if(strchr(cache->container_template,'{')) { + container = mapcache_util_get_tile_key(ctx, tile, cache->container_template, " \r\n\t\f\e\a\b", "#"); + } else { + container = cache->container_template; + } + + pc = _swift_get_connection(ctx, cache, tile); + + if (GC_HAS_ERROR(ctx)) { + return MAPCACHE_FAILURE; + } + conn = pc->connection; + + err = swift_set_container(conn->swift_context, container); + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to set container %s: %d", container, err); + goto cleanup; + } + err = swift_set_object(conn->swift_context, key); + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to set object %s: %d", key, err); + goto cleanup; + } + + err = swift_get_data(conn->swift_context, &size, &data); + if (err == SCERR_AUTH_FAILED) { + mapcache_swift_authenticate(ctx, cache, conn); + if (GC_HAS_ERROR(ctx)) { + goto cleanup; + } + err = swift_get_data(conn->swift_context, &size, &data); + } + + tile->encoded_data = NULL; + + if (err == SCERR_SUCCESS) { + rv = MAPCACHE_SUCCESS; + tile->encoded_data = mapcache_buffer_create(0, ctx->pool); + mapcache_buffer_append(tile->encoded_data, size, data); + } else if (err == SCERR_NOT_FOUND) { + /* simply not found, but no error */ + rv = MAPCACHE_CACHE_MISS; + } else { + ctx->set_error(ctx, 500, "swift: failed to get object data %s: %d", key, err); + rv = MAPCACHE_FAILURE; + } + +cleanup: + conn->swift_context->allocator(data, 0); + + if(GC_HAS_ERROR(ctx)) { + mapcache_connection_pool_invalidate_connection(ctx, pc); + } else { + mapcache_connection_pool_release_connection(ctx, pc); + } + + return rv; +} + +/** + * \brief push tile data to swift + * + * writes the content of mapcache_tile::data to the configured swift object storage + * \private \memberof mapcache_cache_swift + * \sa mapcache_cache::tile_set() + */ +static void _mapcache_cache_swift_set(mapcache_context *ctx, mapcache_cache *pcache, mapcache_tile *tile) { + mapcache_pooled_connection *pc; + mapcache_cache_swift *cache = (mapcache_cache_swift*) pcache; + struct swift_connection *conn; + enum swift_error err; + char *container; + char *key; + + key = mapcache_util_get_tile_key(ctx, tile, cache->key_template, " \r\n\t\f\e\a\b", "#"); + if (GC_HAS_ERROR(ctx)) { + return; + } + + if(strchr(cache->container_template,'{')) { + container = mapcache_util_get_tile_key(ctx, tile, cache->container_template, " \r\n\t\f\e\a\b", "#"); + } else { + container = cache->container_template; + } + + if (!tile->encoded_data) { + tile->encoded_data = tile->tileset->format->write(ctx, tile->raw_image, tile->tileset->format); + GC_CHECK_ERROR(ctx); + } + + pc = _swift_get_connection(ctx, cache, tile); + + if (GC_HAS_ERROR(ctx)) { + return; + } + conn = pc->connection; + + err = swift_set_container(conn->swift_context, container); + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to set container %s: %d", container, err); + goto cleanup; + } + err = swift_set_object(conn->swift_context, key); + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "swift: failed to set object %s: %d", key, err); + goto cleanup; + } + + err = swift_put_data(conn->swift_context, tile->encoded_data->buf, tile->encoded_data->size); + if (err == SCERR_AUTH_FAILED) { + mapcache_swift_authenticate(ctx, cache, conn); + if (GC_HAS_ERROR(ctx)) { + goto cleanup; + } + err = swift_put_data(conn->swift_context, tile->encoded_data->buf, tile->encoded_data->size); + } + + if (err != SCERR_SUCCESS) { + ctx->set_error(ctx, 500, "failed to store tile %s to cache %s due to error %d.", key, cache->cache.name, err); + } + +cleanup: + mapcache_connection_pool_release_connection(ctx, pc); +} + +/** + * \private \memberof mapcache_cache_swift + */ +static void _mapcache_cache_swift_configuration_parse_xml(mapcache_context *ctx, ezxml_t node, mapcache_cache *pcache, mapcache_cfg *config) { + ezxml_t xauth_url,xauth_version,xtenant,xusername,xpassword,xcontainer,xkey,xdebug; + mapcache_cache_swift *cache = (mapcache_cache_swift *)pcache; + + xauth_url = ezxml_child(node, "auth_url"); + xauth_version = ezxml_child(node, "auth_version"); + xtenant = ezxml_child(node, "tenant"); + xusername = ezxml_child(node, "username"); + xpassword = ezxml_child(node, "password"); + xcontainer = ezxml_child(node, "container"); + xkey = ezxml_child(node, "key"); + xdebug = ezxml_child(node, "debug"); + + if (!xauth_url || !xauth_url->txt || ! *xauth_url->txt) { + ctx->set_error(ctx, 400, "cache %s: no ", pcache->name); + return; + } else { + cache->auth_url = apr_pstrdup(ctx->pool, xauth_url->txt); + } + + if (!xauth_version || !xauth_version->txt || ! *xauth_version->txt) { + cache->auth_version = KS_AUTH_V1; + } else { + if (strcasecmp(xauth_version->txt, "1") == 0 || strcasecmp(xauth_version->txt, "v1") == 0) { + cache->auth_version = KS_AUTH_V1; + } else if (strcasecmp(xauth_version->txt, "3") == 0 || strcasecmp(xauth_version->txt, "v3") == 0) { + cache->auth_version = KS_AUTH_V3; + } else { + ctx->set_error(ctx, 400, "cache %s: invalid ", pcache->name); + return; + } + } + + if (!xtenant || !xtenant->txt || ! *xtenant->txt) { + ctx->set_error(ctx, 400, "cache %s: no ", pcache->name); + return; + } else { + cache->tenant = apr_pstrdup(ctx->pool, xtenant->txt); + } + + if (!xusername || !xusername->txt || ! *xusername->txt) { + ctx->set_error(ctx, 400, "cache %s: no ", pcache->name); + return; + } else { + cache->username = apr_pstrdup(ctx->pool, xusername->txt); + } + + if (!xpassword || !xpassword->txt || ! *xpassword->txt) { + ctx->set_error(ctx, 400, "cache %s: no ", pcache->name); + return; + } else { + cache->password = apr_pstrdup(ctx->pool, xpassword->txt); + } + + if (!xcontainer || !xcontainer->txt || ! *xcontainer->txt) { + ctx->set_error(ctx, 400, "cache %s: no ", pcache->name); + return; + } else { + cache->container_template = apr_pstrdup(ctx->pool, xcontainer->txt); + } + + if (!xkey || !xkey->txt || ! *xkey->txt) { + ctx->set_error(ctx, 400, "cache %s: no ", pcache->name); + return; + } else { + cache->key_template = apr_pstrdup(ctx->pool, xkey->txt); + } + + if (xdebug && xdebug->txt && *xdebug->txt) { + cache->debug = strcasecmp(xdebug->txt, "true") == 0; + } +} + +/** + * \private \memberof mapcache_cache_swift + */ +static void _mapcache_cache_swift_configuration_post_config(mapcache_context *ctx, mapcache_cache *cache, mapcache_cfg *cfg) { +} + +/** + * \brief creates and initializes a mapcache_swift_cache + */ +mapcache_cache* mapcache_cache_swift_create(mapcache_context *ctx) { + mapcache_cache_swift *cache; + cache = apr_pcalloc(ctx->pool,sizeof(mapcache_cache_swift)); + + if (!cache) { + ctx->set_error(ctx, 500, "failed to allocate swift cache"); + return NULL; + } + + cache->cache.metadata = apr_table_make(ctx->pool, 3); + cache->cache.type = MAPCACHE_CACHE_SWIFT; + cache->cache._tile_get = _mapcache_cache_swift_get; + cache->cache._tile_exists = _mapcache_cache_swift_has_tile; + cache->cache._tile_set = _mapcache_cache_swift_set; + cache->cache._tile_delete = _mapcache_cache_swift_delete; + cache->cache.configuration_parse_xml = _mapcache_cache_swift_configuration_parse_xml; + cache->cache.configuration_post_config = _mapcache_cache_swift_configuration_post_config; + cache->auth_url = NULL; + cache->tenant = NULL; + cache->username = NULL; + cache->password = NULL; + cache->key_template = NULL; + cache->container_template = NULL; + + return (mapcache_cache*)cache; +} + +#else +mapcache_cache* mapcache_cache_swift_create(mapcache_context *ctx) { + ctx->set_error(ctx,400,"Swift support not compiled in this version"); + return NULL; +} +#endif diff --git a/lib/configuration_xml.c b/lib/configuration_xml.c index e8df80cf..a2ece19a 100644 --- a/lib/configuration_xml.c +++ b/lib/configuration_xml.c @@ -593,6 +593,8 @@ void parseCache(mapcache_context *ctx, ezxml_t node, mapcache_cfg *config) cache = mapcache_cache_couchbase_create(ctx); } else if(!strcmp(type,"riak")) { cache = mapcache_cache_riak_create(ctx); + } else if(!strcmp(type,"swift")) { + cache = mapcache_cache_swift_create(ctx); } else { ctx->set_error(ctx, 400, "unknown cache type %s for cache \"%s\"", type, name); return; diff --git a/lib/keystone-client.c b/lib/keystone-client.c new file mode 100644 index 00000000..d4961f2a --- /dev/null +++ b/lib/keystone-client.c @@ -0,0 +1,1242 @@ +/* + * Based on https://github.com/ukyg9e5r6k7gubiekd6/keystone-client + */ + +#ifdef USE_SWIFT + +#include +#include +#include +#include + +#include "keystone-client.h" + +/* The MIME type representing JavaScript Object Notation */ +#define MIME_TYPE_JSON "application/json" +/* The content-type of the authentication requests we send */ +/* Each of XML and JSON is allowed; we use JSON for brevity and simplicity */ +#define KEYSTONE_AUTH_REQUEST_FORMAT MIME_TYPE_JSON +/* The content-type we desire to receive in authentication responses */ +#define KEYSTONE_AUTH_RESPONSE_FORMAT MIME_TYPE_JSON +/* The portion of a JSON-encoded Keystone credentials POST body preceding the username */ +#define KEYSTONE_AUTH_PAYLOAD_BEFORE_USERNAME "\ +{\n\ + \"auth\":{\n\ + \"passwordCredentials\":{\n\ + \"username\":\"" +/* The portion of a JSON-encoded Keystone credentials POST body succeeding the username and preceding the password */ +#define KEYSTONE_AUTH_PAYLOAD_BEFORE_PASSWORD "\",\n\ + \"password\":\"" +/* The portion of a JSON-encoded Keystone credentials POST body succeeding the password and preceding the tenant name */ +#define KEYSTONE_AUTH_PAYLOAD_BEFORE_TENANT "\"\n\ + },\n\ + \"tenantName\":\"" +/* The portion of a JSON-encoded Keystone credentials POST body succeeding the tenant name */ +#define KEYSTONE_AUTH_PAYLOAD_END "\"\n\ + }\n\ +}" +/* Number of elements in a statically-sized array */ +#define ELEMENTSOF(arr) (sizeof(arr) / sizeof((arr)[0])) + +/** + * Service type names in Keystone's catalog of services. + * Order must match that in enum openstack_service. + */ +static const char *const openstack_service_names[] = { + "identity", /* Keystone */ + "compute", /* Nova */ + "ec2", /* Nova EC2 */ + "object-store", /* Swift */ + "s3", /* Swift S3 */ + "volume", /* Cinder */ + "image" /* Glance */ +}; + +/** + * Service endpoint URL type names, as seen in JSON object keys. + * Order must match that in enum openstack_service_endpoint_type. + */ +static const char *const openstack_service_endpoint_url_type_names[] = { + "publicURL", + "adminURL", + "internalURL" +}; + +/* HUman-friendly names for service endpoint URL types */ +static const char *const openstack_service_endpoint_url_type_friendly_names[] = { + "public", + "admin", + "internal" +}; + +/** + * Default handler for libcurl errors. + */ +static void +default_curl_error_callback(const char *curl_funcname, CURLcode curl_err) +{ + assert(curl_funcname != NULL); + fprintf(stderr, "%s failed: libcurl error code %ld: %s\n", curl_funcname, (long) curl_err, curl_easy_strerror(curl_err)); +} + +/** + * Default handler for libjson errors. + */ +static void +default_json_error_callback(const char *json_funcname, enum json_tokener_error json_err) +{ + assert(json_funcname != NULL); + assert(json_err != json_tokener_success); + assert(json_err != json_tokener_continue); + fprintf(stderr, "%s failed: libjson error %ld: %s\n", json_funcname, (long) json_err, json_tokener_error_desc(json_err)); +} + +/** + * Default handler for Keystone errors. + */ +static void +default_keystone_error_callback(const char *keystone_operation, enum keystone_error keystone_err) +{ + assert(keystone_operation != NULL); + assert(keystone_err != KSERR_SUCCESS); + fprintf(stderr, "Keystone: %s: error %ld\n", keystone_operation, (long) keystone_err); +} + +/** + * Default memory [re-/de-]allocator. + */ +static void * +default_allocator(void *ptr, size_t size) +{ + if (0 == size) { + if (ptr != NULL) { + free(ptr); + } + return NULL; + } + if (NULL == ptr) { + return malloc(size); + } + return realloc(ptr, size); +} + +/** + * To be called at start of user program, while still single-threaded. + * Non-thread-safe and non-re-entrant. + */ +enum keystone_error +keystone_global_init(void) +{ + CURLcode curl_err; + + curl_err = curl_global_init(CURL_GLOBAL_ALL); + if (curl_err != 0) { + /* TODO: Output error indications about detected error in 'res' */ + return KSERR_INIT_FAILED; + } + + return KSERR_SUCCESS; +} + +/** + * To be called at end of user program, while again single-threaded. + * Non-thread-safe and non-re-entrant. + */ +void +keystone_global_cleanup(void) +{ + curl_global_cleanup(); +} + +/** + * To be called by each thread of user program that will use this library, + * before first other use of this library. + * Thread-safe and re-entrant. + */ +enum keystone_error +keystone_start(keystone_context_t *context) +{ + assert(context != NULL); + if (!context->curl_error) { + context->curl_error = default_curl_error_callback; + } + if (!context->json_error) { + context->json_error = default_json_error_callback; + } + if (!context->keystone_error) { + context->keystone_error = default_keystone_error_callback; + } + if (!context->allocator) { + context->allocator = default_allocator; + } + context->pvt.version = KS_AUTH_V1; + context->pvt.curl = curl_easy_init(); + if (NULL == context->pvt.curl) { + /* NOTE: No error code from libcurl, so we assume/invent CURLE_FAILED_INIT */ + context->curl_error("curl_easy_init", CURLE_FAILED_INIT); + return KSERR_INIT_FAILED; + } + + return KSERR_SUCCESS; +} + +/** + * To be called by each thread of user program that will use this library, + * after last other use of this library. + * To be called once per successful call to keystone_start by that thread. + * Thread-safe and re-entrant. + */ +void +keystone_end(keystone_context_t *context) +{ + assert(context != NULL); + assert(context->pvt.curl != NULL); + + curl_easy_cleanup(context->pvt.curl); + context->pvt.curl = NULL; + if (context->pvt.auth_token != NULL) { + context->pvt.auth_token = context->allocator(context->pvt.auth_token, 0); + } + if (context->pvt.auth_payload != NULL) { + context->pvt.auth_payload = context->allocator(context->pvt.auth_payload, 0); + } + if (context->pvt.json_tokeniser != NULL) { + json_tokener_free(context->pvt.json_tokeniser); + context->pvt.json_tokeniser = NULL; + } +} + +/** + * Control whether a proxy (eg HTTP or SOCKS) is used to access the Keystone server. + * Argument must be a URL, or NULL if no proxy is to be used. + */ +enum keystone_error +keystone_set_proxy(keystone_context_t *context, const char *proxy_url) +{ + CURLcode curl_err; + + assert(context != NULL); + assert(context->pvt.curl != NULL); + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_PROXY, (NULL == proxy_url) ? "" : proxy_url); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return KSERR_INVARG; + } + + return KSERR_SUCCESS; +} + +/** + * Control verbose logging to stderr of the actions of this library and the libraries it uses. + * Currently this enables logging to standard error of libcurl's actions. + */ +enum keystone_error +keystone_set_debug(keystone_context_t *context, unsigned int enable_debugging) +{ + CURLcode curl_err; + + assert(context != NULL); + assert(context->pvt.curl != NULL); + + context->pvt.debug = enable_debugging; + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_VERBOSE, enable_debugging ? 1 : 0); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return KSERR_INVARG; + } + + return KSERR_SUCCESS; +} + +/** + * Return the length of the given JSON array. + */ +static unsigned int +json_array_length(keystone_context_t *context, struct json_object *array) +{ + int len; + + assert(context != NULL); + assert(array != NULL); + + if (json_object_is_type(array, json_type_array)) { + len = json_object_array_length(array); + if (len < 0) { + context->keystone_error("JSON array length is negative", KSERR_PARSE); + len = 0; + } else if (len > UINT_MAX) { + context->keystone_error("JSON array length is too large for unsigned int", KSERR_PARSE); + len = 0; + } + } else { + context->keystone_error("JSON object is not an array", KSERR_PARSE); + len = 0; + } + + return (unsigned int) len; +} + +/** + * Return the index'th element of the given JSON array. + */ +static struct json_object * +json_array_get(keystone_context_t *context, struct json_object *array, unsigned int index) +{ + struct json_object *item; + + assert(context != NULL); + assert(array != NULL); + + if (json_object_is_type(array, json_type_array)) { + if (index < json_array_length(context, array)) { + item = json_object_array_get_idx(array, index); + if (NULL == item) { + context->keystone_error("failed to index into JSON array", KSERR_PARSE); + } + } else { + context->keystone_error("JSON array index out of bound", KSERR_PARSE); + item = NULL; + } + } else { + context->keystone_error("JSON object is not an array", KSERR_PARSE); + item = NULL; + } + return item; +} + +/** + * Types of return from an enumerator function for a JSON array. + */ +enum item_callback_return { + CONTINUE = 0, /* Continue iterating */ + STOP = 1 /* Cease iteration */ +}; + +typedef enum item_callback_return item_callback_return_t; + +/** + * A function which receives items from a JSON array. + */ +typedef item_callback_return_t (*item_callback_func_t)(keystone_context_t *context, struct json_object *item, void *callback_arg); + +/** + * Iterate over the given JSON array, passing elements of it to the given iteration function. + * If the iteration function ever returns STOP, return the array element passed to it which caused it to first return STOP. + * If the iteration function returns CONTINUE for each and every array element, return NULL. + * The iteration function is not guaranteed to be called for each and every element in the given array. + */ +static struct json_object * +json_array_find(keystone_context_t *context, struct json_object *array, item_callback_func_t callback, void *callback_arg) +{ + struct json_object *item; + unsigned int i; + + assert(context != NULL); + assert(array != NULL); + assert(callback != NULL); + + i = json_array_length(context, array); + while (i--) { + item_callback_return_t ret; + item = json_array_get(context, array, i); + ret = callback(context, item, callback_arg); + switch (ret) { + case CONTINUE: + break; + case STOP: + return item; + default: + assert(0); + break; + } + } + + return NULL; +} + +const char * +service_name(unsigned int service) +{ + assert(service < ELEMENTSOF(openstack_service_names)); + return openstack_service_names[service]; +} + +const char * +endpoint_url_name(unsigned int endpoint) +{ + assert(endpoint < ELEMENTSOF(openstack_service_endpoint_url_type_friendly_names)); + return openstack_service_endpoint_url_type_friendly_names[endpoint]; +} + +/** + * If the OpenStack service represented by the given JSON object is of the type name given by callback_arg, return STOP. + * Otherwise, if it is of some other type or if its type cannot be determined, return CONTINUE. + */ +static item_callback_return_t +filter_service_by_type(keystone_context_t *context, struct json_object *service, void *callback_arg) +{ + const char *desired_type = (const char *) callback_arg; + struct json_object *service_type; + + assert(context != NULL); + assert(service != NULL); + assert(callback_arg != NULL); + + if (!json_object_is_type(service, json_type_object)) { + context->keystone_error("response.access.serviceCatalog[n] is not an object", KSERR_PARSE); + return CONTINUE; + } + if (!json_object_object_get_ex(service, "type", &service_type)) { + context->keystone_error("response.access.serviceCatalog[n] lacks a 'type' key", KSERR_PARSE); + return CONTINUE; + } + if (!json_object_is_type(service_type, json_type_string)) { + context->keystone_error("response.access.serviceCatalog[n].type is not a string", KSERR_PARSE); + return CONTINUE; + } + if (0 != strcmp(json_object_get_string(service_type), desired_type)) { + return CONTINUE; /* Not the service type we're after */ + } + + return STOP; /* Acceptable */ +} + +/** + * Given a JSON array representing a list of OpenStack services, find the first service of the given-named type. + * If a service of the given type name is found, return it. + * Otherwise, if no service of the given type name is found, return NULL. + */ +struct json_object * +find_service_by_type_name(keystone_context_t *context, struct json_object *services, const char *desired_type) +{ + assert(context != NULL); + assert(services != NULL); + assert(desired_type != NULL); + + return json_array_find(context, services, filter_service_by_type, (void *) desired_type); +} + +/** + * Given a JSON array representing a list of OpenStack services, find the first service of the given type. + * Otherwise, if a service of the given type is found, return it. + * Otherwise, if no service of the given type is found, return NULL. + */ +struct json_object * +find_service_by_type(keystone_context_t *context, struct json_object *services, enum openstack_service desired_type) +{ + assert(context != NULL); + assert(services != NULL); + assert((unsigned int) desired_type < ELEMENTSOF(openstack_service_names)); + assert(openstack_service_names[(unsigned int) desired_type] != NULL); + + return json_array_find(context, services, filter_service_by_type, (void *) openstack_service_names[(unsigned int) desired_type]); +} + +/** + * Given a JSON object representing an endpoint of an OpenStack service and a desired endpoint version, + * if the given endpoint appears to have the given API version, or it has no versionID attribute, return STOP. + * Otherwise, if the endpoint's version is not the desired version, or the endpoint's version is erroneous, return CONTINUE. + */ +static item_callback_return_t +filter_endpoint_by_version(keystone_context_t *context, json_object *endpoint, void *callback_arg) +{ + unsigned int desired_api_version; + struct json_object *endpoint_api_version; + + assert(context != NULL); + assert(endpoint != NULL); + assert(callback_arg != NULL); + + desired_api_version = *((unsigned int *) callback_arg); + if (!json_object_is_type(endpoint, json_type_object)) { + context->keystone_error("response.access.serviceCatalog[n].endpoints[n] is not an object", KSERR_PARSE); + return CONTINUE; + } + if (!json_object_object_get_ex(endpoint, "versionId", &endpoint_api_version)) { + /* Keystone documentation includes a versionID key, but it is not present in the responses I've seen */ + /* context->keystone_error("response.access.serviceCatalog[n].endpoints[n] lacks a 'versionId' key", KSERR_PARSE); */ + return STOP; /* Take a lack of versionID to mean a catch-all */ + } + if (!json_object_is_type(endpoint_api_version, json_type_string)) { + context->keystone_error("response.access.serviceCatalog[n].endpoints[n].versionId is not a string", KSERR_PARSE); + return CONTINUE; /* Version attribute wrong type */ + } + if (json_object_get_double(endpoint_api_version) != desired_api_version) { + return CONTINUE; /* Not the version we're after */ + } + + /* Found the API version we're after */ + return STOP; +} + +/** + * Given a JSON object representing an OpenStack service, find the first endpoint of the given version. + * If an endpoint of the given version is found, return it. + * Otherwise, if no endpoint of the given version is found, return NULL. + */ +static struct json_object * +service_find_endpoint_by_version(keystone_context_t *context, struct json_object *service, unsigned int desired_api_version) +{ + struct json_object *endpoints, *endpoint; + + if (json_object_object_get_ex(service, "endpoints", &endpoints)) { + if (0 == desired_api_version) { + /* No desired API version currently set, so use the first endpoint found */ + endpoint = json_array_get(context, endpoints, 0); + } else { + /* Looking for a certain version of the Swift RESTful API */ + endpoint = json_array_find(context, endpoints, filter_endpoint_by_version, (void *) &desired_api_version); + } + } else { + context->keystone_error("response.access.serviceCatalog[n] lacks an 'endpoints' key", KSERR_PARSE); + endpoint = NULL; /* Lacking the expected key */ + } + + return endpoint; +} + +/** + * Given a JSON object representing an OpenStack service endpoint, return its URL of the given type, if any. + * If the service endpoint has no URL + */ +static const char * +endpoint_url(keystone_context_t *context, struct json_object *endpoint, enum openstack_service_endpoint_url_type endpoint_url_type) +{ + struct json_object *endpoint_public_url; + const char *url_val; + const char *url_type_name; + + assert(context != NULL); + assert(endpoint != NULL); + assert(endpoint_url_type < ELEMENTSOF(openstack_service_endpoint_url_type_names)); + assert(openstack_service_endpoint_url_type_names[(unsigned int) endpoint_url_type] != NULL); + + url_type_name = openstack_service_endpoint_url_type_names[(unsigned int) endpoint_url_type]; + + if (json_object_object_get_ex(endpoint, url_type_name, &endpoint_public_url)) { + if (json_object_is_type(endpoint_public_url, json_type_string)) { + url_val = json_object_get_string(endpoint_public_url); + } else { + context->keystone_error("response.access.serviceCatalog[n].endpoints[n] URL is not a string", KSERR_PARSE); + url_val = NULL; + } + } else { + context->keystone_error("response.access.serviceCatalog[n].endpoints[n] lacks a URL key of the requested type", KSERR_PARSE); + url_val = NULL; + } + + return url_val; +} + +const char * +get_service_url_v1(keystone_context_t *context, enum openstack_service desired_service_type, unsigned int desired_api_version, enum openstack_service_endpoint_url_type endpoint_url_type) +{ + struct json_object *service; + const char *url; + + assert(context != NULL); + + service = find_service_by_type(context, context->pvt.services, desired_service_type); + if (service) { + static struct json_object *endpoint; + endpoint = service_find_endpoint_by_version(context, service, desired_api_version); + if (endpoint) { + url = endpoint_url(context, endpoint, endpoint_url_type); + } else { + url = NULL; + } + } else { + url = NULL; + } + return url; +} + +const char * +get_service_url_v3(keystone_context_t *context, enum openstack_service desired_service_type, enum openstack_service_endpoint_url_type endpoint_url_type) +{ + struct json_object *service; + const char *url = NULL; + + assert(context != NULL); + assert(endpoint_url_type < ELEMENTSOF(openstack_service_endpoint_url_type_friendly_names)); + assert(openstack_service_endpoint_url_type_friendly_names[(unsigned int) endpoint_url_type] != NULL); + + service = find_service_by_type(context, context->pvt.services, desired_service_type); + if (service) { + int num_endpoints; + const char *endpoint_type_name; + struct json_object *endpoints; + + if (!json_object_object_get_ex(service, "endpoints", &endpoints)) { + context->keystone_error("response.token.catalog[n] lacks an 'endpoints' key", KSERR_PARSE); + return NULL; + } + if (!json_object_is_type(endpoints, json_type_array)) { + context->keystone_error("response.token.catalog[n].endpoints is not an array", KSERR_PARSE); + return NULL; + } + + endpoint_type_name = openstack_service_endpoint_url_type_friendly_names[(unsigned int) endpoint_url_type]; + num_endpoints = json_object_array_length(endpoints); + for (int i = 0; i < num_endpoints; ++i) { + struct json_object *endpoint; + struct json_object *interface; + struct json_object *json_url; + + endpoint = json_object_array_get_idx(endpoints, i); + + /* just skip over endpoints of different shape */ + if (!json_object_is_type(endpoint, json_type_object)) { + continue; + } + if (!json_object_object_get_ex(endpoint, "interface", &interface)) { + continue; + } + if (!json_object_is_type(interface, json_type_string)) { + continue; + } + + if (strcasecmp(json_object_get_string(interface), endpoint_type_name) != 0) { + continue; + } + + /* If we have found a fitting endpoint, it must be of the correct shape */ + if(!json_object_object_get_ex(endpoint, "url", &json_url)) { + context->keystone_error("response.token.catalog[n].endpoints[n] lacks an 'url' key", KSERR_PARSE); + return NULL; + } + if (!json_object_is_type(json_url, json_type_string)) { + context->keystone_error("response.token.catalog[n].endpoints[n].url is not a string", KSERR_PARSE); + return NULL; + } + url = json_object_get_string(json_url); + break; + } + } + return url; +} + +/** + * Given a desired service type and version and type of URL, find a service of the given type in Keystone's catalog of services, + * then find an endpoint of that service with the given API version, then return its URL of the given type. + * Return NULL if the service cannot be found, or if no endpoint of the given version can be found, + * or if the service endpoint of the given version has no URL of the given type. + */ +const char * +keystone_get_service_url(keystone_context_t *context, enum openstack_service desired_service_type, unsigned int desired_api_version, enum openstack_service_endpoint_url_type endpoint_url_type) +{ + const char *url; + + if (context->pvt.version == KS_AUTH_V1) { + url = get_service_url_v1(context, desired_service_type, desired_api_version, endpoint_url_type); + } + else if (context->pvt.version == KS_AUTH_V3) { + url = get_service_url_v3(context, desired_service_type, endpoint_url_type); + } + else { + url = NULL; + } + return url; +} + + +size_t process_keystone_response_headers(char *buffer, size_t size, size_t nitems, void *userdata) +{ + keystone_context_t *context = (keystone_context_t *) userdata; + char *token_start; + char *token_end; + size_t token_length; + + if (context->pvt.debug) { + fwrite(buffer, size, nitems, stderr); + } + + /* In keystone v3 the token is in the header fields under X-Subject-Token */ + if (context->pvt.version == KS_AUTH_V3) { + const char *line = buffer; + if (strncasecmp(line, "X-Subject-Token", sizeof("X-Subject-Token") - 1) == 0) { + /* get to value of header */ + token_start = line + sizeof("X-Subject-Token"); + /* skip over whitespace at beginning of header */ + while (isspace(*token_start)) { + ++token_start; + } + /* get end of token value */ + token_end = strstr(line, "\r\n"); + if (token_start < token_end) { + /* calculate length of token and copy it to the context */ + token_length = token_end - token_start; + context->pvt.auth_token = context->allocator( + context->pvt.auth_token, + token_length + 1 /* '\0' */ + ); + if (NULL == context->pvt.auth_token) { + return KSERR_ALLOC_FAILED; /* Allocation failed */ + } + strncpy(context->pvt.auth_token, token_start, token_length); + context->pvt.auth_token[token_length] = '\0'; + } + } + } + + return nitems * size; +} + +static enum keystone_error +process_keystone_json_v1(keystone_context_t *context, struct json_object *response) +{ + struct json_object *access, *token, *id; + if (!json_object_is_type(response, json_type_object)) { + context->keystone_error("response is not an object", KSERR_PARSE); + return KSERR_PARSE; /* Not the expected JSON object */ + } + /* Everything is in an "access" sub-object */ + if (!json_object_object_get_ex(response, "access", &access)) { + context->keystone_error("response lacks 'access' key", KSERR_PARSE); + return KSERR_PARSE; /* Lacking the expected key */ + } + if (!json_object_is_type(access, json_type_object)) { + context->keystone_error("response.access is not an object", KSERR_PARSE); + return KSERR_PARSE; /* Not the expected JSON object */ + } + /* Service catalog */ + if (!json_object_object_get_ex(access, "serviceCatalog", &context->pvt.services)) { + context->keystone_error("response.access lacks 'serviceCatalog' key", KSERR_PARSE); + return KSERR_PARSE; + } + if (!json_object_is_type(context->pvt.services, json_type_array)) { + context->keystone_error("response.access.serviceCatalog not an array", KSERR_PARSE); + return KSERR_PARSE; + } + /* Authentication token */ + if (!json_object_object_get_ex(access, "token", &token)) { + context->keystone_error("reponse.access lacks 'token' key", KSERR_PARSE); + return KSERR_PARSE; /* Lacking the expected key */ + } + if (!json_object_is_type(token, json_type_object)) { + context->keystone_error("response.access.token is not an object", KSERR_PARSE); + return KSERR_PARSE; /* Not the expected JSON object */ + } + if (!json_object_object_get_ex(token, "id", &id)) { + context->keystone_error("response.access.token lacks 'id' key", KSERR_PARSE); + return KSERR_PARSE; /* Lacking the expected key */ + } + if (!json_object_is_type(id, json_type_string)) { + context->keystone_error("response.access.token.id is not a string", KSERR_PARSE); + return KSERR_PARSE; /* Not the expected JSON string */ + } + + context->pvt.auth_token = context->allocator( + context->pvt.auth_token, + json_object_get_string_len(id) + 1 /* '\0' */ + ); + if (NULL == context->pvt.auth_token) { + return KSERR_ALLOC_FAILED; /* Allocation failed */ + } + strcpy(context->pvt.auth_token, json_object_get_string(id)); + return KSERR_SUCCESS; +} + +static enum keystone_error +process_keystone_json_v3(keystone_context_t *context, struct json_object *response) +{ + struct json_object *token; + if (!json_object_is_type(response, json_type_object)) { + context->keystone_error("response is not an object", KSERR_PARSE); + return KSERR_PARSE; /* Not the expected JSON object */ + } + /* Everything is in an "access" sub-object */ + if (!json_object_object_get_ex(response, "token", &token)) { + context->keystone_error("response lacks 'token' key", KSERR_PARSE); + return KSERR_PARSE; /* Lacking the expected key */ + } + if (!json_object_is_type(token, json_type_object)) { + context->keystone_error("response.token is not an object", KSERR_PARSE); + return KSERR_PARSE; /* Not the expected JSON object */ + } + /* Service catalog */ + if (!json_object_object_get_ex(token, "catalog", &context->pvt.services)) { + context->keystone_error("response.token lacks 'catalog' key", KSERR_PARSE); + return KSERR_PARSE; + } + if (!json_object_is_type(context->pvt.services, json_type_array)) { + context->keystone_error("response.token.catalog not an array", KSERR_PARSE); + return KSERR_PARSE; + } + return KSERR_SUCCESS; +} + +/** + * Retrieve the authentication token and service catalog from a now-complete Keystone JSON response, + * and store them in the Keystone context structure for later use. + */ +static enum keystone_error +process_keystone_json(keystone_context_t *context, struct json_object *response) +{ + if (context->pvt.debug) { + json_object_to_file_ext("/dev/stderr", response, JSON_C_TO_STRING_PRETTY); + } + + if (context->pvt.version == KS_AUTH_V1) { + process_keystone_json_v1(context, response); + } + else if (context->pvt.version == KS_AUTH_V3) { + process_keystone_json_v3(context, response); + } + + return KSERR_SUCCESS; +} + +/** + * Process a Keystone authentication response. + * This parses the response and saves copies of the interesting service endpoint URLs. + */ +static size_t +process_keystone_response(void *ptr, size_t size, size_t nmemb, void *userdata) +{ + keystone_context_t *context = (keystone_context_t *) userdata; + const char *body = (const char *) ptr; + size_t len = size * nmemb; + struct json_object *jobj; + enum json_tokener_error json_err; + + assert(context->pvt.json_tokeniser != NULL); + + jobj = json_tokener_parse_ex(context->pvt.json_tokeniser, body, len); + json_err = json_tokener_get_error(context->pvt.json_tokeniser); + if (json_tokener_success == json_err) { + enum keystone_error sc_err = process_keystone_json(context, jobj); + if (sc_err != KSERR_SUCCESS) { + return 0; /* Failed to process JSON. Inform libcurl no data 'handled' */ + } + } else if (json_tokener_continue == json_err) { + /* Complete JSON response not yet received; continue */ + } else { + context->json_error("json_tokener_parse_ex", json_err); + context->keystone_error("failed to parse response", KSERR_PARSE); + return 0; /* Apparent JSON parsing problem. Inform libcurl no data 'handled' */ + } + + return len; /* Inform libcurl that all data were 'handled' */ +} + +/** + * Sets the authorization version. + */ +enum keystone_error +keystone_set_auth_version(keystone_context_t *context, enum keystone_auth_version version) +{ + /* TODO: error check*/ + context->pvt.version = version; + return KSERR_SUCCESS; +} + +/** + * Perform the actual authentication. Needs the payload to be set in the context. + */ +enum keystone_error +authenticate_perform(keystone_context_t *context, const char *url, size_t body_len) { + CURLcode curl_err; + struct curl_slist *headers = NULL; + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_URL, url); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return KSERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_POST, 1L); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return KSERR_URL_FAILED; + } + + /* Append header specifying body content type (since this differs from libcurl's default) */ + headers = curl_slist_append(headers, "Content-Type: " KEYSTONE_AUTH_REQUEST_FORMAT); + + /* Append pseudo-header defeating libcurl's default addition of an "Expect: 100-continue" header. */ + headers = curl_slist_append(headers, "Expect:"); + + if (context->pvt.debug) { + fputs(context->pvt.auth_payload, stderr); + } + + /* Pass the POST request body to libcurl. The data are not copied, so they must persist during the request lifetime. */ + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_POSTFIELDS, context->pvt.auth_payload); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + curl_slist_free_all(headers); + return KSERR_URL_FAILED; + } + + if (body_len == 0 && context->pvt.auth_payload != NULL) { + body_len = strlen(context->pvt.auth_payload); + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_POSTFIELDSIZE, body_len); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + curl_slist_free_all(headers); + return KSERR_URL_FAILED; + } + + /* Add header requesting desired response content type */ + headers = curl_slist_append(headers, "Accept: " KEYSTONE_AUTH_RESPONSE_FORMAT); + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_HTTPHEADER, headers); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + curl_slist_free_all(headers); + return KSERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_HEADERFUNCTION, process_keystone_response_headers); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + curl_slist_free_all(headers); + return KSERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_HEADERDATA, context); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + curl_slist_free_all(headers); + return KSERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_WRITEFUNCTION, process_keystone_response); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + curl_slist_free_all(headers); + return KSERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_WRITEDATA, context); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + curl_slist_free_all(headers); + return KSERR_URL_FAILED; + } + + curl_err = curl_easy_perform(context->pvt.curl); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_perform", curl_err); + curl_slist_free_all(headers); + return KSERR_URL_FAILED; + } + + curl_slist_free_all(headers); + + if (NULL == context->pvt.auth_token) { + return KSERR_AUTH_REJECTED; + } + + return KSERR_SUCCESS; +} + + +/** + * Authenticate against a Keystone authentication service with the given tenant and user names and password. + * This yields an authorisation token, which is then used to access all Swift services. + */ +enum keystone_error +authenticate_v1(keystone_context_t *context, const char *url, const char *tenant_name, const char *username, const char *password) +{ + size_t body_len; + + assert(context != NULL); + assert(context->pvt.curl != NULL); + assert(url != NULL); + assert(tenant_name != NULL); + assert(username != NULL); + assert(password != NULL); + + body_len = + strlen(KEYSTONE_AUTH_PAYLOAD_BEFORE_USERNAME) + + strlen(username) + + strlen(KEYSTONE_AUTH_PAYLOAD_BEFORE_PASSWORD) + + strlen(password) + + strlen(KEYSTONE_AUTH_PAYLOAD_BEFORE_TENANT) + + strlen(tenant_name) + + strlen(KEYSTONE_AUTH_PAYLOAD_END) + ; + + /* Create or reset the JSON tokeniser */ + if (NULL == context->pvt.json_tokeniser) { + context->pvt.json_tokeniser = json_tokener_new(); + if (NULL == context->pvt.json_tokeniser) { + context->keystone_error("json_tokener_new failed", KSERR_INIT_FAILED); + return KSERR_INIT_FAILED; + } + } else { + json_tokener_reset(context->pvt.json_tokeniser); + } + + /* Generate POST request body containing the authentication credentials */ + context->pvt.auth_payload = context->allocator( + context->pvt.auth_payload, + body_len + + 1 /* '\0' */ + ); + + if (NULL == context->pvt.auth_payload) { + return KSERR_ALLOC_FAILED; + } + + sprintf(context->pvt.auth_payload, "%s%s%s%s%s%s%s", + KEYSTONE_AUTH_PAYLOAD_BEFORE_USERNAME, + username, + KEYSTONE_AUTH_PAYLOAD_BEFORE_PASSWORD, + password, + KEYSTONE_AUTH_PAYLOAD_BEFORE_TENANT, + tenant_name, + KEYSTONE_AUTH_PAYLOAD_END + ); + + /* set common */ + return authenticate_perform(context, url, body_len); +} + + +enum keystone_error +authenticate_v3_build_token_request(keystone_context_t *context, const char *tenant_name, const char *username, const char *password, size_t *body_len) +{ + enum keystone_error status = KSERR_SUCCESS; + const char *payload = NULL; + struct json_object* root = NULL; + struct json_object* auth = NULL; + struct json_object* auth_identity = NULL; + struct json_object* auth_identity_methods = NULL; + struct json_object* auth_identity_methods_password = NULL; + struct json_object* auth_identity_password = NULL; + struct json_object* auth_identity_password_user = NULL; + struct json_object* auth_identity_password_user_domain = NULL; + struct json_object* auth_scope = NULL; + struct json_object* auth_scope_project = NULL; + struct json_object* method = NULL; + struct json_object* pw = NULL; + struct json_object* un = NULL; + struct json_object* domain_id = NULL; + struct json_object* project_id = NULL; + + *body_len = 0; + + /* + { + "auth": { + "identity": { + "methods": [ + "password" + ], + "password": { + "user": { + "password": password, + "name": username, + "domain": { + "id": "default" + } + } + } + }, + "scope": { + "project": { + "id": tenant_name + } + } + } + } + */ + + if (!(root = json_object_new_object())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + + if (!(auth = json_object_new_object())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(root, "auth", auth); + + if (!(auth_identity = json_object_new_object())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth, "identity", auth_identity); + + if (!(auth_identity_methods = json_object_new_array())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_identity, "methods", auth_identity_methods); + + if (!(method = json_object_new_string("password"))) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_array_add(auth_identity_methods, method); + + if (!(auth_identity_password = json_object_new_object())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_identity, "password", auth_identity_password); + + if (!(auth_identity_password_user = json_object_new_object())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_identity_password, "user", auth_identity_password_user); + + if (!(pw = json_object_new_string(password))) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_identity_password_user, "password", pw); + + if (!(un = json_object_new_string(username))) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_identity_password_user, "name", un); + + if (!(auth_identity_password_user_domain = json_object_new_object())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_identity_password_user, "domain", auth_identity_password_user_domain); + + if (!(domain_id = json_object_new_string("default"))) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_identity_password_user_domain, "id", domain_id); + + if (!(auth_scope = json_object_new_object())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth, "scope", auth_scope); + + if (!(auth_scope_project = json_object_new_object())) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_scope, "project", auth_scope_project); + + if (!(project_id = json_object_new_string(tenant_name))) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + json_object_object_add(auth_scope_project, "id", project_id); + + /* payload does not need to be freed */ + payload = json_object_to_json_string_ext(root, 0); + if (payload != NULL) { + *body_len = strlen(payload); + } + + /* Generate POST request body containing the authentication credentials */ + context->pvt.auth_payload = context->allocator( + context->pvt.auth_payload, + *body_len + 1 /* '\0' */ + ); + + if (NULL == context->pvt.auth_payload) { + status = KSERR_ALLOC_FAILED; + goto cleanup; + } + + strcpy(context->pvt.auth_payload, payload); + +cleanup: + json_object_put(root); + return status; +} + +enum keystone_error +authenticate_v3(keystone_context_t *context, const char *url, const char *tenant_name, const char *username, const char *password) +{ + char buf[512]; + const char *version_string; + const char *slash; + size_t url_len; + size_t body_len; + enum keystone_error status; + assert(context != NULL); + assert(context->pvt.curl != NULL); + assert(url != NULL); + assert(tenant_name != NULL); + assert(username != NULL); + assert(password != NULL); + + /* Create or reset the JSON tokeniser */ + if (NULL == context->pvt.json_tokeniser) { + context->pvt.json_tokeniser = json_tokener_new(); + if (NULL == context->pvt.json_tokeniser) { + context->keystone_error("json_tokener_new failed", KSERR_INIT_FAILED); + return KSERR_INIT_FAILED; + } + } else { + json_tokener_reset(context->pvt.json_tokeniser); + } + + status = authenticate_v3_build_token_request(context, tenant_name, username, password, &body_len); + + if (status != KSERR_SUCCESS) { + context->keystone_error("authenticate_v3_build_token_request failed", KSERR_INIT_FAILED); + return status; + } + + url_len = strlen(url); + + /* append slash if not ended with slash */ + if (url[url_len - 1] == '/') { + slash = ""; + } else { + slash = "/"; + } + + /* append "v3" if necessary */ + if (strncmp(url + url_len - sizeof("v3/") + 1, "v3/", sizeof("v3/") - 1) == 0 || strncmp(url + url_len - sizeof("v3") + 1, "v3", sizeof("v3") - 1) == 0) { + version_string = ""; + } else { + version_string = "v3/"; + } + snprintf(buf, sizeof buf, "%s%s%sauth/tokens", url, slash, version_string); + + /* send actual authentication request */ + return authenticate_perform(context, buf, body_len); +} + +/** + * Authenticate against a Keystone authentication service with the given tenant and user names and password. + * This yields an authorisation token, which is then used to access all Swift services. + */ +enum keystone_error keystone_authenticate(keystone_context_t *context, const char *url, const char *tenant_name, const char *username, const char *password) +{ + if (context->pvt.version == KS_AUTH_V1) { + return authenticate_v1(context, url, tenant_name, username, password); + } + else if (context->pvt.version == KS_AUTH_V3) { + return authenticate_v3(context, url, tenant_name, username, password); + } + return KSERR_INVARG; +} + + +/** + * Return the previously-acquired Keystone authentication token, if any. + * If no authentication token has previously been acquired, return NULL. + */ +const char * +keystone_get_auth_token(keystone_context_t *context) +{ + assert(context != NULL); + + return context->pvt.auth_token; +} + +#endif /* USE_SWIFT */ diff --git a/lib/swift-client.c b/lib/swift-client.c new file mode 100644 index 00000000..e3257297 --- /dev/null +++ b/lib/swift-client.c @@ -0,0 +1,820 @@ +/* + * Based on https://github.com/ukyg9e5r6k7gubiekd6/swift-client + */ + +#include +#include +#include +#include +#include + +#include "swift-client.h" + +/** + * The maximum length in bytes of a multi-byte UTF-8 sequence. + * + */ +#define UTF8_SEQUENCE_MAXLEN 6 + +/* Prefix to be prepended to Swift metadata key names in order to generate HTTP headers */ +#define SWIFT_METADATA_PREFIX "X-Object-Meta-" +/* Name of HTTP header used to pass authentication token to Swift server */ +#define SWIFT_AUTH_HEADER_NAME "X-Auth-Token" + +#ifdef min +#undef min +#endif +#define min(a, b) (((a) < (b)) ? (a) : (b)) + +/** + * Default handler for POSIX errors which set errno. + */ +static void +default_errno_callback(const char *funcname, int errno_val) +{ + assert(funcname != NULL); + errno = errno_val; + perror(funcname); +} + +/** + * Default handler for libcurl errors. + */ +static void +default_curl_error_callback(const char *curl_funcname, CURLcode curl_err) +{ + assert(curl_funcname != NULL); + fprintf(stderr, "%s failed: libcurl error code %ld: %s\n", curl_funcname, (long) curl_err, curl_easy_strerror(curl_err)); +} + +/** + * Default memory [re-/de-]allocator. + */ +static void * +default_allocator(void *ptr, size_t size) +{ + if (0 == size) { + if (ptr != NULL) { + free(ptr); + } + return NULL; + } + if (NULL == ptr) { + return malloc(size); + } + return realloc(ptr, size); +} + +/** + * To be called at start of user program, while still single-threaded. + * Non-thread-safe and non-re-entrant. + */ +enum swift_error +swift_global_init(void) +{ + CURLcode curl_err; + + curl_err = curl_global_init(CURL_GLOBAL_ALL); + if (curl_err != 0) { + /* TODO: Output error indications about detected error in 'res' */ + return SCERR_INIT_FAILED; + } + + return SCERR_SUCCESS; +} + +/** + * To be called at end of user program, while again single-threaded. + * Non-thread-safe and non-re-entrant. + */ +void +swift_global_cleanup(void) +{ + curl_global_cleanup(); +} + +/** + * To be called by each thread of user program that will use this library, + * before first other use of this library. + * Thread-safe and re-entrant. + */ +enum swift_error +swift_start(swift_context_t *context) +{ + assert(context != NULL); + + if (NULL == context->errno_error) { + context->errno_error = default_errno_callback; + } + if (NULL == context->curl_error) { + context->curl_error = default_curl_error_callback; + } + if (NULL == context->allocator) { + context->allocator = default_allocator; + } + context->pvt.curl = curl_easy_init(); + if (NULL == context->pvt.curl) { + /* NOTE: No error code from libcurl, so we assume/invent CURLE_FAILED_INIT */ + context->curl_error("curl_easy_init", CURLE_FAILED_INIT); + return SCERR_INIT_FAILED; + } + + return SCERR_SUCCESS; +} + +/** + * To be called by each thread of user program that will use this library, + * after last other use of this library. + * To be called once per successful call to swift_start by that thread. + * Thread-safe and re-entrant. + */ +void +swift_end(swift_context_t *context) +{ + assert(context != NULL); + + curl_easy_cleanup(context->pvt.curl); + context->pvt.curl = NULL; + if (context->pvt.auth_token != NULL) { + context->pvt.auth_token = context->allocator(context->pvt.auth_token, 0); + } + if (context->pvt.base_url != NULL) { + context->pvt.base_url = context->allocator(context->pvt.base_url, 0); + } + if (context->pvt.container != NULL) { + context->pvt.container = context->allocator(context->pvt.container, 0); + } + if (context->pvt.object != NULL) { + context->pvt.object = context->allocator(context->pvt.object, 0); + } +} + +/** + * Control whether a proxy (eg HTTP or SOCKS) is used to access the Swift server. + * Argument must be a URL, or NULL if no proxy is to be used. + */ +enum swift_error +swift_set_proxy(swift_context_t *context, const char *proxy_url) +{ + CURLcode curl_err; + + assert(context != NULL); + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_PROXY, (NULL == proxy_url) ? "" : proxy_url); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_INVARG; + } + + return SCERR_SUCCESS; +} + +/** + * Control verbose logging to stderr of the actions of this library and the libraries it uses. + * Currently this enables logging to standard error of libcurl's actions. + */ +enum swift_error +swift_set_debug(swift_context_t *context, unsigned int enable_debugging) +{ + CURLcode curl_err; + + assert(context != NULL); + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_VERBOSE, enable_debugging ? 1 : 0); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_INVARG; + } + + return SCERR_SUCCESS; +} + +/** + * Set the current Swift server URL. This must not contain any path information. + */ +enum swift_error +swift_set_url(swift_context_t *context, const char *url) +{ + size_t url_len; + + assert(context != NULL); + + url_len = strlen(url); + context->pvt.base_url = context->allocator(context->pvt.base_url, url_len + 1 /* '\0' */); + if (NULL == context->pvt.base_url) { + return SCERR_ALLOC_FAILED; + } + strcpy(context->pvt.base_url, url); + context->pvt.base_url_len = url_len; + + return SCERR_SUCCESS; +} + +/** + * Control whether an HTTPS server's certificate is required to chain to a trusted CA cert. + */ +enum swift_error +swift_verify_cert_trusted(swift_context_t *context, unsigned int require_trusted_cert) +{ + CURLcode curl_err; + + assert(context != NULL); + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_SSL_VERIFYPEER, (long) require_trusted_cert); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + + return SCERR_SUCCESS; +} + +/** + * Control whether an HTTPS server's hostname is required to match its certificate's hostname. + */ +enum swift_error +swift_verify_cert_hostname(swift_context_t *context, unsigned int require_matching_hostname) +{ + CURLcode curl_err; + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_SSL_VERIFYHOST, (long) require_matching_hostname); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + + return SCERR_SUCCESS; +} + +/** + * Set the value of the authentication token to be supplied with requests. + * This should have been obtained previously from a separate authentication service. + */ +enum swift_error +swift_set_auth_token(swift_context_t *context, const char *auth_token) +{ + assert(context != NULL); + assert(auth_token != NULL); + + context->pvt.auth_token = context->allocator(context->pvt.auth_token, strlen(auth_token) + 1 /* '\0' */); + if (NULL == context->pvt.auth_token) { + return SCERR_ALLOC_FAILED; + } + strcpy(context->pvt.auth_token, auth_token); + + return SCERR_SUCCESS; +} + +/** + * Set the name of the current Swift container. + */ +enum swift_error +swift_set_container(swift_context_t *context, char *container_name) +{ + assert(context != NULL); + assert(container_name != NULL); + context->pvt.container = context->allocator(context->pvt.container, strlen(container_name) + 1 /* '\0' */); + if (NULL == context->pvt.container) { + return SCERR_ALLOC_FAILED; + } + strcpy(context->pvt.container, container_name); + + return SCERR_SUCCESS; +} + +/** + * Set the name of the current Swift object. + */ +enum swift_error +swift_set_object(swift_context_t *context, char *object_name) +{ + assert(context != NULL); + assert(object_name != NULL); + context->pvt.object = context->allocator(context->pvt.object, strlen(object_name) + 1 /* '\0' */); + if (NULL == context->pvt.object) { + return SCERR_ALLOC_FAILED; + } + strcpy(context->pvt.object, object_name); + + return SCERR_SUCCESS; +} + +/** + * Generate a Swift URL from the current base URL, account, container and object. + */ +static enum swift_error +make_url(swift_context_t *context, enum swift_operation operation) +{ + size_t url_len = context->pvt.base_url_len; + + assert(context != NULL); + assert(context->pvt.container != NULL); + assert(context->pvt.base_url != NULL); + assert(context->pvt.base_url_len); + + switch (operation) { + case HAS_OBJECT: + case PUT_OBJECT: + case GET_OBJECT: + case SET_OBJECT_METADATA: + case DELETE_OBJECT: + assert(context->pvt.object != NULL); + url_len += + 1 /* '/' */ + + strlen(context->pvt.object) + ; + /* no break: fall thru */ + case CREATE_CONTAINER: + case LIST_CONTAINER: + case SET_CONTAINER_METADATA: + case DELETE_CONTAINER: + url_len += + 1 /* '/' */ + + strlen(context->pvt.container) + ; + break; + default: + assert(0); + return SCERR_INVARG; + } + url_len++; /* '\0' */ + + context->pvt.base_url = context->allocator(context->pvt.base_url, url_len); + if (NULL == context->pvt.base_url) { + return SCERR_ALLOC_FAILED; + } + + switch (operation) { + case CREATE_CONTAINER: + case LIST_CONTAINER: + case SET_CONTAINER_METADATA: + case DELETE_CONTAINER: + sprintf( + context->pvt.base_url + context->pvt.base_url_len, + "/%s", + context->pvt.container + ); + break; + case HAS_OBJECT: + case PUT_OBJECT: + case GET_OBJECT: + case SET_OBJECT_METADATA: + case DELETE_OBJECT: + sprintf( + context->pvt.base_url + context->pvt.base_url_len, + "/%s/%s", + context->pvt.container, + context->pvt.object + ); + break; + default: + assert(0); + return SCERR_INVARG; + } + + return SCERR_SUCCESS; +} + +/** + * Execute a Swift request using the current protocol, hostname, API version, account, container and object, + * and using the given HTTP method. + * This is the portion of the request code that is common to all Swift API operations. + * This function consumes headers. + */ +static enum swift_error +swift_request(swift_context_t *context, enum swift_operation operation, struct curl_slist *headers, supply_data_func_t produce_request_callback, void *produce_request_callback_arg, receive_data_func_t consume_response_callback, void *consume_response_callback_arg) +{ + CURLcode curl_err; + enum swift_error sc_err; + + assert(context != NULL); + + sc_err = make_url(context, operation); + if (sc_err != SCERR_SUCCESS) { + return sc_err; + } + + /* FIXME: Failed attempt to prevent libcurl from uselessly using chunked transfer encoding for empty request bodies */ + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_POSTFIELDS, NULL); + if (CURLE_OK != curl_err) { + return SCERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_POSTFIELDSIZE, 0); + if (CURLE_OK != curl_err) { + return SCERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_READFUNCTION, produce_request_callback); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_READDATA, produce_request_callback_arg); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_WRITEFUNCTION, consume_response_callback); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_WRITEDATA, consume_response_callback_arg); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_URL, context->pvt.base_url); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + /* Set HTTP request method */ + { + CURLoption curl_opt; + union { + long longval; + const char *stringval; + } curl_param; + + switch (operation) { + case LIST_CONTAINER: + case GET_OBJECT: + /* method GET */ + curl_opt = CURLOPT_HTTPGET; + curl_param.longval = 1L; + break; + case CREATE_CONTAINER: + case PUT_OBJECT: + /* method PUT */ + curl_opt = CURLOPT_UPLOAD; /* Causes libcurl to use HTTP PUT method */ + curl_param.longval = 1L; + break; + case SET_CONTAINER_METADATA: + case SET_OBJECT_METADATA: + /* method POST */ + curl_opt = CURLOPT_POST; + curl_param.longval = 1L; + break; + case DELETE_CONTAINER: + case DELETE_OBJECT: + /* method DELETE */ + curl_opt = CURLOPT_CUSTOMREQUEST; /* Causes libcurl to use the given-named HTTP method */ + curl_param.stringval = "DELETE"; + break; + case HAS_OBJECT: + curl_opt = CURLOPT_NOBODY; + curl_param.longval = 1L; + break; + default: + /* Unrecognised Swift operation type */ + assert(0); + return SCERR_INVARG; + } + curl_err = curl_easy_setopt(context->pvt.curl, curl_opt, curl_param); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + } + /* Append common headers to those requested by caller */ + { + char *header = NULL; + + header = context->allocator( + header, + strlen(SWIFT_AUTH_HEADER_NAME) + + 2 /* ": " */ + + strlen(context->pvt.auth_token) + + 1 /* '\0' */ + ); + if (NULL == header) { + return SCERR_ALLOC_FAILED; + } + sprintf(header, SWIFT_AUTH_HEADER_NAME ": %s", context->pvt.auth_token); + headers = curl_slist_append(headers, header); + context->allocator(header, 0); + } + + /* Append pseudo-header defeating libcurl's default addition of an "Expect: 100-continue" header. */ + headers = curl_slist_append(headers, "Expect:"); + + curl_err = curl_easy_setopt(context->pvt.curl, CURLOPT_HTTPHEADER, headers); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_setopt", curl_err); + return SCERR_URL_FAILED; + } + + curl_err = curl_easy_perform(context->pvt.curl); + if (CURLE_OK != curl_err) { + context->curl_error("curl_easy_perform", curl_err); + return SCERR_URL_FAILED; + } else { + long response_code; + curl_easy_getinfo(context->pvt.curl, CURLINFO_RESPONSE_CODE, &response_code); + if (401 == response_code) { + return SCERR_AUTH_FAILED; + } else if (404 == response_code) { + return SCERR_NOT_FOUND; + } else if (response_code >= 400 && response_code < 500) { + return SCERR_INVALID_REQ; + } else if (response_code >= 500) { + return SCERR_SERVER_ERROR; + } + } + + curl_slist_free_all(headers); + + return SCERR_SUCCESS; +} + +/** + * Null response consumer. Completely ignores the entire response. + */ +static size_t +ignore_response(void *ptr, size_t size, size_t nmemb, void *userdata) +{ + return size * nmemb; +} + +/** + * Null request producer. Supplies a zero-length body. + */ +static size_t +empty_request(void *ptr, size_t size, size_t nmemb, void *arg) +{ + return 0; +} + +/** + * Check the existence of an object. + */ +enum swift_error +swift_has(swift_context_t *context, int *exists) +{ + enum swift_error err; + assert(context != NULL); + assert(context->pvt.auth_token != NULL); + + *exists = 0; + + err = swift_request(context, HAS_OBJECT, NULL, empty_request, NULL, NULL, NULL); + + if (err == SCERR_SUCCESS) { + *exists = 1; + } + return err; +} + +/** + * Retrieve an object from Swift and pass its data to the given callback function. + */ +enum swift_error +swift_get(swift_context_t *context, receive_data_func_t receive_data_callback, void *callback_arg) +{ + assert(context != NULL); + assert(context->pvt.auth_token != NULL); + assert(receive_data_callback != NULL); + + return swift_request(context, GET_OBJECT, NULL, empty_request, NULL, receive_data_callback, callback_arg); +} + +static size_t +write_data_to_file(void *ptr, size_t size, size_t nmemb, void *stream) +{ + assert(ptr != NULL); + assert(stream != NULL); + + return fwrite(ptr, size, nmemb, (FILE *) stream); +} + +/** + * Retrieve an object from Swift and place its data in the given-named file. + */ +enum swift_error +swift_get_file(swift_context_t *context, const char *filename) +{ + FILE *stream; + enum swift_error swift_err; + + assert(context != NULL); + assert(filename != NULL); + + stream = fopen(filename, "wb"); + if (NULL == stream) { + context->errno_error("fopen", errno); + swift_err = SCERR_FILEIO_FAILED; + } else { + swift_err = swift_get(context, write_data_to_file, stream); + if (fclose(stream) != 0) { + if (SCERR_SUCCESS == swift_err) { + swift_err = SCERR_FILEIO_FAILED; + } + } + } + + return swift_err; +} + +struct write_buffer { + struct swift_context *context; + char *ptr; + size_t size; +}; + +static size_t +write_data_to_buffer(void *ptr, size_t size, size_t nmemb, void *userdata) +{ + struct write_buffer *buffer = (struct write_buffer*) userdata; + size_t bytesize = size * nmemb; + + buffer->ptr = buffer->context->allocator(buffer->ptr, buffer->size + bytesize + 1); + memcpy(&(buffer->ptr[buffer->size]), ptr, bytesize); + buffer->size += bytesize; + buffer->ptr[buffer->size] = 0; + return bytesize; +} + +/** + * Retrieve an object from Swift and place its data in the variable. + */ +enum swift_error +swift_get_data(swift_context_t *context, size_t *size, void **data) +{ + enum swift_error swift_err; + struct write_buffer buffer; + + assert(context != NULL); + assert(data != NULL); + + buffer.context = context; + buffer.ptr = NULL; + buffer.size = 0; + + swift_err = swift_get(context, write_data_to_buffer, &buffer); + if (buffer.ptr == NULL) { + if (SCERR_SUCCESS == swift_err) { + swift_err = SCERR_FILEIO_FAILED; + } + } + + *size = buffer.size; + *data = buffer.ptr; + + return swift_err; +} + +/** + * Create a Swift container with the current container name. + */ +enum swift_error +swift_create_container(swift_context_t *context, size_t metadata_count, const wchar_t **metadata_names, const wchar_t **metadata_values) +{ + assert(context != NULL); + assert(context->pvt.auth_token != NULL); + + return swift_request(context, CREATE_CONTAINER, NULL, empty_request, NULL, ignore_response, NULL); +} + +/** + * Delete the Swift container with the current container name. + */ +enum swift_error +swift_delete_container(swift_context_t *context) +{ + assert(context != NULL); + assert(context->pvt.auth_token != NULL); + + return swift_request(context, DELETE_CONTAINER, NULL, empty_request, NULL, ignore_response, NULL); +} + +/** + * Insert or update an object in Swift using the data supplied by the given callback function. + * Optionally, also attach a set of metadata {name, value} tuples to the object. + * metadata_count specifies the number of {name, value} tuples to be set. This may be zero. + * If metadata_count is non-zero, metadata_names and metadata_values must be arrays, each of length metadata_count, specifying the {name, value} tuples. + */ +enum swift_error +swift_put(swift_context_t *context, supply_data_func_t supply_data_callback, void *callback_arg) +{ + struct curl_slist *headers = NULL; + + assert(context != NULL); + assert(context->pvt.auth_token != NULL); + + return swift_request(context, PUT_OBJECT, headers, supply_data_callback, callback_arg, ignore_response, NULL); +} + +static size_t +supply_data_from_file(void *ptr, size_t size, size_t nmemb, void *stream) +{ + assert(ptr != NULL); + assert(stream != NULL); + return fread(ptr, size, nmemb, (FILE *) stream); +} + +/** + * Insert or update an object in Swift using the data in the given-names file. + * Optionally, also attach a set of metadata {name, value} tuples to the object. + * metadata_count specifies the number of {name, value} tuples to be set. This may be zero. + * If metadata_count is non-zero, metadata_names and metadata_values must be arrays, each of length metadata_count, specifying the {name, value} tuples. + */ +enum swift_error +swift_put_file(swift_context_t *context, const char *filename) +{ + FILE *stream; + enum swift_error swift_err; + + assert(context != NULL); + assert(filename != NULL); + + stream = fopen(filename, "rb"); + if (NULL == stream) { + context->errno_error("fopen", errno); + swift_err = SCERR_FILEIO_FAILED; + } else { + swift_err = swift_put(context, supply_data_from_file, stream); + if (fclose(stream) != 0) { + if (SCERR_SUCCESS == swift_err) { + swift_err = SCERR_FILEIO_FAILED; + } + } + } + + return swift_err; +} + +struct data_from_mem_args { + const unsigned char *ptr; + size_t nleft; +}; + +static size_t +supply_data_from_memory(void *ptr, size_t size, size_t nmemb, void *cookie) +{ + struct data_from_mem_args *args = (struct data_from_mem_args *) cookie; + size_t len = min(size * nmemb, args->nleft); + + assert(ptr != NULL); + + memcpy(ptr, args->ptr, len); + args->ptr += len; + args->nleft -= len; + + return len; +} + +/** + * Insert or update an object in Swift using the size bytes of data located in memory at ptr. + * Optionally, also attach a set of metadata {name, value} tuples to the object. + * metadata_count specifies the number of {name, value} tuples to be set. This may be zero. + * If metadata_count is non-zero, metadata_names and metadata_values must be arrays, each of length metadata_count, specifying the {name, value} tuples. + */ +enum swift_error +swift_put_data(swift_context_t *context, const void *ptr, size_t size) +{ + struct data_from_mem_args args; + + assert(context != NULL); + + args.ptr = ptr; + args.nleft = size; + + return swift_put(context, supply_data_from_memory, &args); +} + +/** + * Insert or update metadata for the current object. + * metadata_count specifies the number of {name, value} tuples to be set. + * metadata_names and metadata_values must be arrays, each of length metadata_count, specifying the {name, value} tuples. + */ +enum swift_error +swift_set_metadata(swift_context_t *context, size_t metadata_count, const wchar_t **metadata_names, const wchar_t **metadata_values) +{ + struct curl_slist *headers = NULL; + + assert(context != NULL); + + if (0 == metadata_count) { + return SCERR_SUCCESS; /* Nothing to do */ + } + + return swift_request(context, SET_OBJECT_METADATA, headers, empty_request, NULL, ignore_response, NULL); +} + +/** + * Delete the Swift object with the current container and object names. + */ +enum swift_error +swift_delete_object(swift_context_t *context) +{ + assert(context != NULL); + assert(context->pvt.auth_token != NULL); + + return swift_request(context, DELETE_OBJECT, NULL, empty_request, NULL, ignore_response, NULL); +}