diff --git a/RELEASE.md b/RELEASE.md index 0fd3e7a..1a71278 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -23,6 +23,13 @@ files respectively, in the configure/ directory of the appropriate release of th Release Notes ============= +R2-4 (22-Aug-2024) +---- +* Added header file and transferred class declaration to there. +* Added curl usage: driver can use either curl or GraphicksMagick to get image from URL. +* Added WITH_CURL compilation option to compile with or without libcurl. +* Add [documentation](docs/ADURL/ADURL.rst) about curl configuration records. + R2-3 (26-May-2021) ---- * Converted documentation to ReST, include in documentation on github.io. diff --git a/configure/CONFIG_SITE b/configure/CONFIG_SITE index 4df968f..34f7f83 100644 --- a/configure/CONFIG_SITE +++ b/configure/CONFIG_SITE @@ -15,6 +15,7 @@ # Set CHECK_RELEASE to WARN to perform consistency checking but # continue building anyway if conflicts are found. CHECK_RELEASE = YES +# WITH_CURL = YES # Set this when you only want to compile this application # for a subset of the cross-compiled target architectures diff --git a/docs/ADURL/ADURL.rst b/docs/ADURL/ADURL.rst index f1bdfea..7d3f179 100644 --- a/docs/ADURL/ADURL.rst +++ b/docs/ADURL/ADURL.rst @@ -12,15 +12,30 @@ Introduction This is an :doc:`../index` driver for reading images from a URL. It can be used to read images from Web cameras, `Axis video -servers`_, or simply from a disk file. It reads the images using -`GraphicsMagick`_ and can thus read images encoded in any of the -formats supported by GraphicsMagick, such as JPEG, TIFF, PNG, etc. +servers`_, or simply from a disk file. It can read the images +using either `GraphicsMagick`_ or `Curl`_ (if compiled with the +WITH_CURL=YES option on CONFIG_SITE and toggling the UseCurl record) +and can thus read images encoded in any of the formats supported +by GraphicsMagick, such as JPEG, TIFF, PNG, etc. The driver simply reads images from the specified URL at the rate determined by the AcquirePeriod. Web cameras and Axis video servers have a URL address from which the current image can be read. There are often several addresses for different image sizes. +If configured correctly, curl can support both url redirection and authentication. +To avoid tedious, repetitive and error prone configuration processes, curl can +have a configuration file with all `curl options`_ in the format key = value. Example: + +| CURLOPT_HTTPAUTH = 2 +| CURLOPT_USERNAME = User +| CURLOPT_PASSWORD = PassWord + +Each curl option must have a value adequate to the asynParamType that the option +is implemented. For example, CURLOPT_HTTPAUTH = CURLAUTH_BASIC will not work. Also, +the file only configure asyn-implemented options. Unfortunately, it does not configure +curl options that are not yet implemented in the driver. + This driver inherits from :doc:`../ADCore/ADDriver`. It implements many of the parameters in `asynNDArrayDriver.h`_ and in `ADArrayDriver.h`_. It also implements a number of parameters that are specific to the URL @@ -54,6 +69,24 @@ the standard driver parameters. **NOTE: If this value is set to 0 or too small a value can result in the driver using 100% of the CPU and becoming unresponsive to EPICS.** + * - NDFilePath + - $(P)$(R)CfgFilePath + - Path of directory where to find curl configuration file. + * - NDFilePathExists + - $(P)$(R)CfgFilePathExists_RBV + - Is set to one if driver thinks the file exists. Is set to 0 otherwise. + * - NDFileName + - $(P)$(R)CfgFileName + - Curl configuration file name. The file should contain values to be put + into each record in the format: + key = value + Example: + CURLOPT_HTTPAUTH = 2 + CURLOPT_USERNAME = User + CURLOPT_PASSWORD = PassWord + * - NDFullFileName + - $(P)$(R)CfgFullFileName_RBV + - Full filename appended with filepath. URL driver specific parameters @@ -79,6 +112,35 @@ those in asynNDArrayDriver.h and ADDriver.h. * - Selects which os the 10 URLs to read from. - $(P)$(R)URLSelect - mbbo + * - Determines if image is acquired from URL with GraphicsMagick or Curl. + - $(P)$(R)UseCurl + - bo + * - Sets curl's HTTP authentication method. + - $(P)$(R)CurlOptHTTPAuth, $(P)$(R)CurlOptHTTP_RBV + - mbbo, mbbi + * - Toggle curl's hostname verification in SSL certificate. + - $(P)$(R)CurlOptSSLVerifyHost, $(P)$(R)CurlOptSSLVerifyHost_RBV + - mbbo, mbbi + * - Toggle curl's SSL certificate verification. + - $(P)$(R)CurlOptSSLVerifyPerr, $(P)$(R)CurlOptSSLVerifyPeer_RBV + - bo, bi + * - Set curl's authentication username. + - $(P)$(R)CurlOptUserName + - waveform + * - Set curl's authentication password. + - $(P)$(R)CurlOptPassword + - waveform + * - Shows if driver has read permission to file. + - $(P)$(R)CfgFileValid_RBV + - bi + * - $(P)$(R)CurlLoadConfig + - Loads configuration from file in $(P)$(R)CfgFullFileName_RBV. BEWARE: + this will probably make the setpoint and readback curl option records have + different values. For example, if you set $(P)$(R)CurlOptHTTPAuth and then + load a CurlConfigFile that changes it, $(P)$(R)CurlOptHTTPAuth_RBV is going + to have a different file. Also, $(P)$(R)CurlOptUserName and + $(P)$(R)CurlOptUserPassword are going to be set in the curl option, but there + is no readback for them. The URLs for Web cameras and video servers are typically long strings, which are difficult to remember and to type. Thus, for convenience @@ -174,5 +236,6 @@ connected to analog camera through an Axis video server. .. _GraphicsMagick: http://www.graphicsmagick.org/ .. _ADDriver: ../areaDetectorDoc.html#ADDriver .. _Axis video servers: http://www.axis.com/ - +.. _Curl: https://curl.se/ +.. _curl options: https://curl.se/libcurl/c/curl_easy_setopt.html diff --git a/iocs/urlIOC/iocBoot/iocURLDriver/st_base.cmd b/iocs/urlIOC/iocBoot/iocURLDriver/st_base.cmd index 60ad48c..54a6a88 100755 --- a/iocs/urlIOC/iocBoot/iocURLDriver/st_base.cmd +++ b/iocs/urlIOC/iocBoot/iocURLDriver/st_base.cmd @@ -28,6 +28,11 @@ epicsEnvSet("EPICS_DB_INCLUDE_PATH", "$(ADCORE)/db") URLDriverConfig("$(PORT)", 0, 0) dbLoadRecords("$(ADURL)/db/URLDriver.template","P=$(PREFIX),R=cam1:,PORT=$(PORT),ADDR=0,TIMEOUT=1") +#### If using curl uncomment this +## CurlUsage.template includes another template file under $(ADURL)/db/ +# epicsEnvSet("EPICS_DB_INCLUDE_PATH", "$(EPICS_DB_INCLUDE_PATH):$(ADURL)/db/") +# Load curl configuration records +# dbLoadRecords("$(ADURL)/db/CurlUsage.template","P=$(PREFIX),R=cam1:,PORT=$(PORT),ADDR=0,TIMEOUT=1") # Create a standard arrays plugin. NDStdArraysConfigure("Image1", 3, 0, "$(PORT)", 0) diff --git a/urlApp/Db/CurlConfigFile.template b/urlApp/Db/CurlConfigFile.template new file mode 100644 index 0000000..806d2f0 --- /dev/null +++ b/urlApp/Db/CurlConfigFile.template @@ -0,0 +1,91 @@ +#=================================================================# +# Template file: CurlConfigFile.template +# Made to load files. Inspired but not completely copied from NDFile.template +# Marco Montevechi +# aug 11, 2024 + +################################################################### +# These records control Config file loading # +################################################################### + +# File path. +record(waveform, "$(P)$(R)CfgFilePath") +{ + field(PINI, "YES") + field(DTYP, "asynOctetWrite") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))FILE_PATH") + field(FTVL, "CHAR") + field(NELM, "256") + info(autosaveFields, "VAL") +} + +record(waveform, "$(P)$(R)CfgFilePath_RBV") +{ + field(DTYP, "asynOctetRead") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))FILE_PATH") + field(FTVL, "CHAR") + field(NELM, "256") + field(SCAN, "I/O Intr") +} + +record(bi, "$(P)$(R)CfgFilePathExists_RBV") +{ + field(DTYP, "asynInt32") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))FILE_PATH_EXISTS") + field(ZNAM, "No") + field(ZSV, "MAJOR") + field(ONAM, "Yes") + field(OSV, "NO_ALARM") + field(SCAN, "I/O Intr") +} + +# Filename +record(waveform, "$(P)$(R)CfgFileName") +{ + field(PINI, "YES") + field(DTYP, "asynOctetWrite") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))FILE_NAME") + field(FTVL, "CHAR") + field(NELM, "256") + info(autosaveFields, "VAL") +} + +record(waveform, "$(P)$(R)CfgFileName_RBV") +{ + field(DTYP, "asynOctetRead") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))FILE_NAME") + field(FTVL, "CHAR") + field(NELM, "256") + field(SCAN, "I/O Intr") +} + +# Full filename, including path +record(waveform, "$(P)$(R)CfgFullFileName_RBV") +{ + field(DTYP, "asynOctetRead") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))FULL_FILE_NAME") + field(FTVL, "CHAR") + field(NELM, "512") + field(SCAN, "I/O Intr") +} + +# Full filename, including path +record(bi, "$(P)$(R)CfgFileValid_RBV") +{ + field(DTYP, "asynInt32") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))FILE_IS_VALID") + field(ZNAM, "No") + field(ZSV, "MAJOR") + field(ONAM, "Yes") + field(OSV, "NO_ALARM") + field(SCAN, "I/O Intr") +} + +record (bo, "$(P)$(R)CurlLoadConfig") +{ + field(DESC, "Load curl configuration from file") + field(DTYP, "asynInt32") + field(ZNAM, "0") + field(ONAM, "1") + field(OUT, "@asyn($(PORT),$(ADDR),$(TIMEOUT))CURL_LOAD_CONFIG") +} \ No newline at end of file diff --git a/urlApp/Db/CurlUsage.template b/urlApp/Db/CurlUsage.template new file mode 100644 index 0000000..dda1cd8 --- /dev/null +++ b/urlApp/Db/CurlUsage.template @@ -0,0 +1,139 @@ +# Records to toggle curl functionalities + +include "CurlConfigFile.template" + +record (bo, "$(P)$(R)UseCurl") +{ + field(DESC, "Toggle curl usage") + field(DTYP, "asynInt32") + field(PINI, "YES") + field(ZNAM, "NO") + field(ONAM, "YES") + field(OUT, "@asyn($(PORT),$(ADDR),$(TIMEOUT))USE_CURL") + info(autosaveFields, "VAL") +} + +record (mbbo, "$(P)$(R)CurlOptHTTPAuth") +{ + field(DESC, "Set CURLOPT_HTTPAUTH") + field(DTYP, "asynInt32") + field(ZRST, "CURLAUTH_BASIC") + field(ZRVL, "0") + field(ONST, "CURLAUTH_DIGEST") + field(ONVL, "1") + field(TWST, "CURLAUTH_DIGEST_IE") + field(TWVL, "2") + field(THST, "CURLAUTH_BEARER") + field(THVL, "3") + field(FRST, "CURLAUTH_NEGOTIATE") + field(FRVL, "4") + field(FVST, "CURLAUTH_NTLM") + field(FVVL, "5") + field(SXST, "CURLAUTH_NTLM_WB") + field(SXVL, "6") + field(SVST, "CURLAUTH_ANY") + field(SVVL, "7") + field(EIST, "CURLAUTH_ANYSAFE") + field(EIVL, "8") + field(NIST, "CURLAUTH_ONLY") + field(NIVL, "9") + field(TEST, "CURLAUTH_AWS_SIGV4") + field(TEVL, "10") + field(OUT, "@asyn($(PORT),$(ADDR),$(TIMEOUT))ASYN_CURLOPT_HTTPAUTH") +} + +record (mbbi, "$(P)$(R)CurlOptHTTPAuth_RBV") +{ + field(DESC, "Set CURLOPT_HTTPAUTH") + field(DTYP, "asynInt32") + field(ZRST, "CURLAUTH_BASIC") + field(ZRVL, "0") + field(ONST, "CURLAUTH_DIGEST") + field(ONVL, "1") + field(TWST, "CURLAUTH_DIGEST_IE") + field(TWVL, "2") + field(THST, "CURLAUTH_BEARER") + field(THVL, "3") + field(FRST, "CURLAUTH_NEGOTIATE") + field(FRVL, "4") + field(FVST, "CURLAUTH_NTLM") + field(FVVL, "5") + field(SXST, "CURLAUTH_NTLM_WB") + field(SXVL, "6") + field(SVST, "CURLAUTH_ANY") + field(SVVL, "7") + field(EIST, "CURLAUTH_ANYSAFE") + field(EIVL, "8") + field(NIST, "CURLAUTH_ONLY") + field(NIVL, "9") + field(TEST, "CURLAUTH_AWS_SIGV4") + field(TEVL, "10") + field(SCAN, "I/O Intr") + field(INP, "@asyn($(PORT),$(ADDR),$(TIMEOUT))ASYN_CURLOPT_HTTPAUTH") +} + +record (mbbo, "$(P)$(R)CurlOptSSLVerifyHost") +{ + field(DESC, "Verify Host SSL certificate") + field(DTYP, "asynInt32") + field(ZRST, "NO") + field(ZRVL, "0") + field(ONST, "YES") + field(ONVL, "1") + field(TWST, "YES") + field(TWVL, "2") + field(OUT, "@asyn($(PORT),$(ADDR),$(TIMEOUT))ASYN_CURLOPT_SSL_VERIFYHOST") +} + +record (mbbi, "$(P)$(R)CurlOptSSLVerifyHost_RBV") +{ + field(DESC, "Verify Host SSL certificate") + field(DTYP, "asynInt32") + field(ZRST, "NO") + field(ZRVL, "0") + field(ONST, "YES") + field(ONVL, "1") + field(TWST, "YES") + field(TWVL, "2") + field(SCAN, "I/O Intr") + field(INP, "@asyn($(PORT),$(ADDR),$(TIMEOUT))ASYN_CURLOPT_SSL_VERIFYHOST") +} + +record (bo, "$(P)$(R)CurlOptSSLVerifyPeer") +{ + field(DESC, "Verify SSL peer") + field(DTYP, "asynInt32") + field(PINI, "YES") + field(ZNAM, "NO") + field(ONAM, "YES") + field(OUT, "@asyn($(PORT),$(ADDR),$(TIMEOUT))ASYN_CURLOPT_SSL_VERIFYPEER") +} + +record (bi, "$(P)$(R)CurlOptSSLVerifyPeer_RBV") +{ + field(DESC, "Verify SSL peer") + field(DTYP, "asynInt32") + field(PINI, "YES") + field(ZNAM, "NO") + field(ONAM, "YES") + field(SCAN, "I/O Intr") + field(INP, "@asyn($(PORT),$(ADDR),$(TIMEOUT))ASYN_CURLOPT_SSL_VERIFYPEER") +} + +record(waveform, "$(P)$(R)CurlOptUserName") +{ + field(DESC, "Username for auth. Try not to use this.") + field(DTYP, "asynOctetWrite") + field(INP, "@asyn($(PORT),$(ADDR),$(TIMEOUT))ASYN_CURLOPT_USERNAME") + field(FTVL, "CHAR") + field(NELM, "128") +} + +record(waveform, "$(P)$(R)CurlOptPassword") +{ + field(DESC, "Password for auth. Try not to use this.") + field(DTYP, "asynOctetWrite") + field(INP, "@asyn($(PORT),$(ADDR),$(TIMEOUT))ASYN_CURLOPT_PASSWORD") + field(FTVL, "CHAR") + field(NELM, "128") +} \ No newline at end of file diff --git a/urlApp/Db/Makefile b/urlApp/Db/Makefile index 9c89075..57df990 100644 --- a/urlApp/Db/Makefile +++ b/urlApp/Db/Makefile @@ -12,6 +12,10 @@ include $(TOP)/configure/CONFIG # databases, templates, substitutions like this DB += URLDriver.template +ifeq ($(WITH_CURL), YES) + DB += CurlUsage.template + DB += CurlConfigFile.template +endif #---------------------------------------------------- # If .db template is not named *.template add diff --git a/urlApp/src/Makefile b/urlApp/src/Makefile index 735bc62..df39585 100644 --- a/urlApp/src/Makefile +++ b/urlApp/src/Makefile @@ -43,6 +43,11 @@ ifeq ($(WITH_GRAPHICSMAGICK), YES) include $(ADCORE)/ADApp/commonLibraryMakefile endif +ifeq ($(WITH_CURL),YES) + USR_CXXFLAGS += -DADURL_USE_CURL + USR_SYS_LIBS += curl +endif + #============================= include $(TOP)/configure/RULES diff --git a/urlApp/src/URLDriver.cpp b/urlApp/src/URLDriver.cpp index 1d0efa4..65c2da0 100644 --- a/urlApp/src/URLDriver.cpp +++ b/urlApp/src/URLDriver.cpp @@ -22,43 +22,28 @@ #include using namespace Magick; -#include "ADDriver.h" - #include - -#define DRIVER_VERSION 2 -#define DRIVER_REVISION 3 -#define DRIVER_MODIFICATION 0 +#include static const char *driverName = "URLDriver"; -/** URL driver; reads images from URLs, such as Web cameras and Axis video servers, but also files, etc. */ -class URLDriver : public ADDriver { -public: - URLDriver(const char *portName, int maxBuffers, size_t maxMemory, - int priority, int stackSize); - - /* These are the methods that we override from ADDriver */ - virtual asynStatus writeInt32(asynUser *pasynUser, epicsInt32 value); - virtual void report(FILE *fp, int details); - void URLTask(); /**< Should be private, but gets called from C, so must be public */ - -protected: - int URLName; - #define FIRST_URL_DRIVER_PARAM URLName - -private: - /* These are the methods that are new to this class */ - virtual asynStatus readImage(); - - /* Our data */ - Image image; - epicsEventId startEventId; - epicsEventId stopEventId; -}; +#ifdef ADURL_USE_CURL +/** Called by class constructor to initialize curl handle pointer + * and set the writeCallback function and read buffer. +*/ +void URLDriver::initializeCurl(){ + curl_global_init(CURL_GLOBAL_DEFAULT); + this->curl = curl_easy_init(); + if (!curl){ + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: ERROR, cannot initialize curl pointer.\n", driverName, __func__); + } -#define URLNameString "URL_NAME" + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &curlBuffer); +} +#endif asynStatus URLDriver::readImage() { @@ -75,11 +60,31 @@ asynStatus URLDriver::readImage() int depth; const char *map; static const char *functionName = "readImage"; - + getStringParam(URLName, sizeof(URLString), URLString); + #ifdef ADURL_USE_CURL + int use_curl; + getIntegerParam(useCurl, &use_curl); + #endif if (strlen(URLString) == 0) return(asynError); try { + #ifdef ADURL_USE_CURL + if (use_curl) { + this->curlBuffer.clear(); + this->res = curl_easy_perform(curl); + if (res != CURLE_OK){ + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: curl read error %d\n", + driverName, functionName, res); + return(asynError); + } + Blob blob(&curlBuffer[0], curlBuffer.size()); + image.read(blob); + } else { + image.read(URLString); + } + #else image.read(URLString); + #endif imageType = image.type(); depth = image.depth(); nrows = image.rows(); @@ -102,8 +107,8 @@ asynStatus URLDriver::readImage() colorMode = NDColorModeRGB1; break; default: - asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, - "%s:%s: unknown ImageType=%d\n", + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: unknown ImageType=%d\n", driverName, functionName, imageType); return(asynError); break; @@ -123,8 +128,8 @@ asynStatus URLDriver::readImage() storageType = IntegerPixel; break; default: - asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, - "%s:%s: unsupported depth=%d\n", + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: unsupported depth=%d\n", driverName, functionName, depth); return(asynError); break; @@ -134,7 +139,7 @@ asynStatus URLDriver::readImage() pImage = this->pArrays[0]; asynPrint(this->pasynUserSelf, ASYN_TRACEIO_DRIVER, "%s:%s: reading URL=%s, dimensions=[%lu,%lu,%lu], ImageType=%d, depth=%d\n", - driverName, functionName, URLString, + driverName, functionName, URLString, (unsigned long)dims[0], (unsigned long)dims[1], (unsigned long)dims[2], imageType, depth); image.write(0, 0, ncols, nrows, map, storageType, pImage->pData); pImage->pAttributeList->add("ColorMode", "Color mode", NDAttrInt32, &colorMode); @@ -149,12 +154,12 @@ asynStatus URLDriver::readImage() } catch(std::exception &error) { - asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR, - "%s:%s: error reading URL=%s\n", + asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: error reading URL=%s\n", driverName, functionName, error.what()); return(asynError); } - + return(asynSuccess); } @@ -288,7 +293,7 @@ void URLDriver::URLTask() } } - + /** Called when asyn clients call pasynInt32->write(). * This function performs actions for some parameters, including ADAcquire, ADColorMode, etc. * For all parameters it sets the value in the parameter library and calls any registered callbacks.. @@ -316,6 +321,16 @@ asynStatus URLDriver::writeInt32(asynUser *pasynUser, epicsInt32 value) /* Send the stop event */ epicsEventSignal(this->stopEventId); } + #ifdef ADURL_USE_CURL + } else if (function==curlOptHttpAuth) { + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CurlHttpOptions[value]); + } else if (function==curlOptSSLVerifyHost) { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, (long)value); + } else if (function==curlOptSSLVerifyPeer) { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, value); + } else if (function==curlLoadConfig) { + this->loadConfigFile(); + #endif } else { /* If this parameter belongs to a base class call its method */ if (function < FIRST_URL_DRIVER_PARAM) status = ADDriver::writeInt32(pasynUser, value); @@ -335,9 +350,187 @@ asynStatus URLDriver::writeInt32(asynUser *pasynUser, epicsInt32 value) return status; } +#ifdef ADURL_USE_CURL +/** Only actually implemented if compiled with WITH_CURL = YES. + * Called when asyn clients call pasynOctet->write(). + * In this particular driver, this function sets the curl string configuration options. + * For all parameters it sets the value in the parameter library and calls any registered callbacks.. + * \param[in] pasynUser pasynUser structure that encodes the reason and address. + * \param[in] value Address of the string to write. + * \param[in] nChars Number of characters to write. + * \param[out] nActual Number of characters actually written. */ +asynStatus URLDriver::writeOctet(asynUser *pasynUser, const char *value, size_t nChars, size_t *nActual) +{ + + int addr = 0; + int function = pasynUser->reason; + int status = 0; + char param[MAXCURLSTRCHARS] = {'\0'}; + + status |= setStringParam(addr, function, (char *)value); + + if (function < FIRST_URL_DRIVER_PARAM) { + + if (function==NDFileName or function==NDFilePath){ + this->completeFullPath(); + } + + status |= ADDriver::writeOctet(pasynUser, value, nChars, nActual); + + } else if (function == curlOptUserName) { + getStringParam(curlOptUserName, MAXCURLSTRCHARS, param); + curl_easy_setopt(curl, CURLOPT_USERNAME, param); + } else if (function == curlOptPassword) { + getStringParam(curlOptPassword, MAXCURLSTRCHARS, param); + curl_easy_setopt(curl, CURLOPT_PASSWORD, param); + } else if (function == URLName) { + getStringParam(URLName, MAXCURLSTRCHARS, param); + curl_easy_setopt(curl, CURLOPT_URL, param); + } + + callParamCallbacks(addr); + *nActual = nChars; + return (asynStatus)status; + +} + +/* Called each time filePath or fileName is changed to check if file is accessible. + Should only be called inside writeOctet() so needn't call callbacks, although it + set the fileIsValid parameter to either 0 or 1.*/ +asynStatus URLDriver::completeFullPath() +{ + + char fullFileName[MAX_FILENAME_LEN]; + int status = 0; + const char * functionName = "completeFullPath"; + struct stat file; + + status = ADDriver::createFileName(2*MAX_FILENAME_LEN, fullFileName); + + if (status) { + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: Failed to create full filename.\n", driverName, functionName); + return (asynStatus)status; + } + + setStringParam(NDFullFileName, fullFileName); + + /* Check if file is accessible */ + if (stat(fullFileName, &(file)) == 0 && + S_ISREG(file.st_mode) && + access(fullFileName, R_OK) == 0){setIntegerParam(fileIsValid, 1);} + else {setIntegerParam(fileIsValid, 0);} + + return asynSuccess; + +} + +/* Parses options from a configuration file and sets all the parameters that it can + with the options. Only works for asyn-implemented curl options because it calls either + URLDriver::writeInt32 or URLDriver::writeOctet for each option.*/ +asynStatus URLDriver::loadConfigFile() +{ + + const char * functionName = "loadConfigFile"; + char fullFileName[MAX_FILENAME_LEN]; + std::ifstream file; + std::string line, key, value; int valueInt; + asynParamType type; + int param, status = 0; + size_t nActual = 0; + + getStringParam(NDFullFileName, MAX_FILENAME_LEN, fullFileName); + + file.open(fullFileName); + if (!file) { + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: ERROR, cannot open file %s.\n", driverName, functionName, fullFileName); + return asynError; + } + + while (getline(file, line)) { + key = line.substr(0,line.find("=")); + value = line.substr(line.find("=")+1, line.back()); + + /* Taking spaces out of strings */ + key.erase(std::remove(key.begin(), key.end(), ' '), key.end()); + value.erase(std::remove(value.begin(), value.end(), ' '), value.end()); + + /* Finding which asyn parameter corersponds to option */ + key = "ASYN_" + key; + asynPortDriver::findParam(key.c_str(), ¶m); + /* asynUser to call writeOctet or writeInt32 later */ + asynUser tempUser{.reason = param}; + + /* If param is credential, set curlOption but don't set asyn record*/ + if (param == curlOptUserName) { + curl_easy_setopt(curl, CURLOPT_USERNAME, value.c_str()); + continue; + } else if (param == curlOptPassword) { + curl_easy_setopt(curl, CURLOPT_PASSWORD, value.c_str()); + continue; + } + + if (param == -1){ + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: ERROR, cannot find parameter %s from config file." + " Is this parameter implemented?\n", + driverName, functionName, key.c_str()); + + return asynError; + } + + asynPortDriver::getParamType(param, &type); + switch (type) { + case asynParamInt32: + try { + valueInt = stoi(value); + } catch (std::invalid_argument&) { + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: ERROR, cannot convert value %s to int for" + " parameter %s\n", + driverName, functionName, value.c_str(), key.c_str()); + return asynError; + } + status |= this->writeInt32(&tempUser, (epicsInt32)valueInt); + break; + case asynParamOctet: + status |= this->writeOctet(&tempUser, value.c_str(), value.size(), &nActual); + break; + default: + asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, + "%s:%s: ERROR, parameter %s is of asynParam type" + " %d. This function only deals with asynParamOctet (%d)" + " and asynParamInt32 (%d).\n", + driverName, functionName, key.c_str(), type, + asynParamOctet, asynParamInt32); + return asynError; + } + + + } + + file.close(); + return (asynStatus)status; + +} + +/* Is called after curl_easy_perform to read from url and store output into *userp buffer + in this particular driver, userp is std::vector * this->curlBuffer. + To avoid accumulating infinite information in memory, buffer should always be cleaned + before calling curl_easy_perform. If can't be cleaned inside this function though, because + it seems it is called several times for each curl_easy_perform call.*/ +size_t URLDriver::curlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) +{ + int totalSize = size * nmemb; + + ((std::vector*)userp)->insert(((std::vector*)userp)->end(), (char*)contents, (char*)contents + totalSize); + return totalSize; +} + +#endif - /** Report status of the driver. * Prints details about the driver if details>0. * It then calls the ADDriver::report() method. @@ -374,7 +567,7 @@ void URLDriver::report(FILE *fp, int details) * \param[in] priority The thread priority for the asyn port driver thread if ASYN_CANBLOCK is set in asynFlags. * \param[in] stackSize The stack size for the asyn port driver thread if ASYN_CANBLOCK is set in asynFlags. */ -URLDriver::URLDriver(const char *portName, int maxBuffers, size_t maxMemory, +URLDriver::URLDriver(const char *portName, int maxBuffers, size_t maxMemory, int priority, int stackSize) : ADDriver(portName, 1, 0, maxBuffers, maxMemory, @@ -403,10 +596,32 @@ URLDriver::URLDriver(const char *portName, int maxBuffers, size_t maxMemory, createParam(URLNameString, asynParamOctet, &URLName); + #ifdef ADURL_USE_CURL + createParam(UseCurlString, asynParamInt32, &useCurl); + createParam(CurlLoadConfigString, asynParamInt32, &curlLoadConfig); + createParam(CurlFileIsValidString, asynParamInt32, &fileIsValid); + createParam(CurlOptHttpAuthString, asynParamInt32, &curlOptHttpAuth); + createParam(CurlOptSSLVerifyHostString, asynParamInt32, &curlOptSSLVerifyHost); + createParam(CurlOptSSLVerifyPeerString, asynParamInt32, &curlOptSSLVerifyPeer); + createParam(CurlOptUserNameString, asynParamOctet, &curlOptUserName); + createParam(CurlOptPasswordString, asynParamOctet, &curlOptPassword); + + setIntegerParam(useCurl, 0); + setIntegerParam(curlOptHttpAuth, 0); + setIntegerParam(curlOptSSLVerifyHost, 2L); + setIntegerParam(curlOptSSLVerifyPeer, 1); + setStringParam(curlOptUserName, "\0"); + setStringParam(curlOptPassword, "\0"); + + /* FileTemplate parameter won't use complicated templates here */ + setStringParam(NDFileTemplate, "%s%s"); + this->initializeCurl(); + #endif + /* Set some default values for parameters */ status = setStringParam (ADManufacturer, "URL Driver"); status |= setStringParam (ADModel, "GraphicsMagick"); - epicsSnprintf(versionString, sizeof(versionString), "%d.%d.%d", + epicsSnprintf(versionString, sizeof(versionString), "%d.%d.%d", DRIVER_VERSION, DRIVER_REVISION, DRIVER_MODIFICATION); setStringParam(NDDriverVersion, versionString); setStringParam(ADSDKVersion, MagickLibVersionText); @@ -431,7 +646,7 @@ URLDriver::URLDriver(const char *portName, int maxBuffers, size_t maxMemory, } /** Configuration command, called directly or from iocsh */ -extern "C" int URLDriverConfig(const char *portName, int maxBuffers, size_t maxMemory, +extern "C" int URLDriverConfig(const char *portName, int maxBuffers, size_t maxMemory, int priority, int stackSize) { /* Initialize GraphicsMagick */ diff --git a/urlApp/src/URLDriver.h b/urlApp/src/URLDriver.h new file mode 100644 index 0000000..61768b6 --- /dev/null +++ b/urlApp/src/URLDriver.h @@ -0,0 +1,79 @@ +#include "ADDriver.h" + +#ifdef ADURL_USE_CURL + #include + #include + #include + #include +#endif + +#define DRIVER_VERSION 2 +#define DRIVER_REVISION 4 +#define DRIVER_MODIFICATION 0 + +/** URL driver; reads images from URLs, such as Web cameras and Axis video servers, but also files, etc. */ +class URLDriver : public ADDriver { +public: + URLDriver(const char *portName, int maxBuffers, size_t maxMemory, + int priority, int stackSize); + + /* These are the methods that we override from ADDriver */ + virtual asynStatus writeInt32(asynUser *pasynUser, epicsInt32 value); + virtual void report(FILE *fp, int details); + void URLTask(); /**< Should be private, but gets called from C, so must be public */ + + #ifdef ADURL_USE_CURL + virtual asynStatus writeOctet(asynUser *pasynUser, const char *value, size_t nChars, size_t *nActual); + asynStatus completeFullPath(); + asynStatus loadConfigFile(); + void initializeCurl(); + static size_t curlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp); + #endif + +protected: + int URLName; + #define FIRST_URL_DRIVER_PARAM URLName + + #ifdef ADURL_USE_CURL + int useCurl; + int curlLoadConfig; + int fileIsValid; + int curlOptHttpAuth; + int curlOptSSLVerifyHost; + int curlOptSSLVerifyPeer; + int curlOptUserName; + int curlOptPassword; + #define MAXCURLSTRCHARS 128 + CURL *curl = NULL; + CURLcode res; + std::vector curlBuffer; + + /*Array to translate CurlHttpAuth options*/ + long unsigned int CurlHttpOptions [11] = {CURLAUTH_BASIC, CURLAUTH_DIGEST, CURLAUTH_DIGEST_IE, CURLAUTH_BEARER, + CURLAUTH_NEGOTIATE, CURLAUTH_NTLM, CURLAUTH_NTLM_WB, CURLAUTH_ANY, + CURLAUTH_ANYSAFE, CURLAUTH_ONLY, CURLAUTH_AWS_SIGV4}; + #endif + +private: + /* These are the methods that are new to this class */ + virtual asynStatus readImage(); + + /* Our data */ + Image image; + epicsEventId startEventId; + epicsEventId stopEventId; + +}; + +#define URLNameString "URL_NAME" + +#ifdef ADURL_USE_CURL + #define UseCurlString "USE_CURL" + #define CurlLoadConfigString "CURL_LOAD_CONFIG" + #define CurlFileIsValidString "FILE_IS_VALID" + #define CurlOptHttpAuthString "ASYN_CURLOPT_HTTPAUTH" + #define CurlOptSSLVerifyHostString "ASYN_CURLOPT_SSL_VERIFYHOST" + #define CurlOptSSLVerifyPeerString "ASYN_CURLOPT_SSL_VERIFYPEER" + #define CurlOptUserNameString "ASYN_CURLOPT_USERNAME" + #define CurlOptPasswordString "ASYN_CURLOPT_PASSWORD" +#endif