HTTPS post request to Azure endpoint

Hi,

I'm trying to make a https post request to my Azure endpoint with a nrf9160 using Zephyr. I've first made the post request in a python script to check if everything is working on the Azure side, and that seems to work fine, I've included the python script below:

(The script first gets a bearer token and then uses that token to make a post request)

import requests
from datetime import datetime

# Define variables
app_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
app_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
dce_uri = "https://log-ingestion-xxxx.westeurope-1.ingest.monitor.azure.com"
dcr_immutable_id = "dcr-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
table = "table_name"

# Obtain a bearer token
scope = "https://monitor.azure.com//.default"
body = f"client_id={app_id}&scope={scope}&client_secret={app_secret}&grant_type=client_credentials"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
uri = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
response = requests.post(uri, data=body, headers=headers)
bearer_token = response.json()["access_token"]

# Generate and send some data
body = [{
        "device" : "2405235235211243",
        "timestamp" : "2022-02-19T15:30:49.4337994Z",
        "log_level": "DEBUG",
        "log_message": "This is a test"
    }]
headers = {"Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json"}
uri = f"{dce_uri}/dataCollectionRules/{dcr_immutable_id}/streams/Custom-{table}?api-version=2023-01-01"
upload_response = requests.post(uri, json=body, headers=headers)

To implement this on the nrf9160 using Zephyr, I've modified the https_client sample, but I keep getting error -2 on getaddrinfo(). I've included the code below:

/*
 * Copyright (c) 2020 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
 */

#include <string.h>
#include <zephyr/kernel.h>
#include <stdlib.h>
#include <zephyr/net/socket.h>
#include <modem/nrf_modem_lib.h>
#include <zephyr/net/tls_credentials.h>
#include <modem/pdn.h>
#include <modem/lte_lc.h>
#include <modem/modem_key_mgmt.h>

#define HTTPS_PORT "443"

#define HTTPS_HOSTNAME "https://login.microsoftonline.com/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/oauth2/v2.0/token"
#define BODY "client_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&scope=https://monitor.azure.com//.default&client_secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxgrant_type=client_credentials"

// #define HTTP_HEAD                                                                                  \
// 	"HEAD / HTTP/1.1\r\n"                                                                      \
// 	"Host: " HTTPS_HOSTNAME ":" HTTPS_PORT "\r\n"                                              \
// 	"Connection: close\r\n\r\n"

#define HTTP_HEAD "POST / HTTP/1.1\r\n"\
		"Host: "HTTPS_HOSTNAME"\r\n"\
		"Connection: close\r\n"\
	    "Content-type: application/x-www-form-urlencoded\r\n"\
		"\r\n"\
		""BODY"\r\n\r\n"

#define HTTP_HEAD_LEN (sizeof(HTTP_HEAD) - 1)

#define HTTP_HDR_END "\r\n\r\n"

#define RECV_BUF_SIZE 2048
#define TLS_SEC_TAG 42
#define PDN_IPV6_WAIT_MS 1000

static const char send_buf[] = HTTP_HEAD;
static char recv_buf[RECV_BUF_SIZE];
static K_SEM_DEFINE(pdn_ipv6_up_sem, 0, 1);

/* Certificate for `example.com` */
static const char cert[] = {
#include "../cert/DigiCertGlobalRootCA.pem"
};

BUILD_ASSERT(sizeof(cert) < KB(4), "Certificate too large");

/* Provision certificate to modem */
int cert_provision(void)
{
	int err;
	bool exists;
	int mismatch;

	/* It may be sufficient for you application to check whether the correct
	 * certificate is provisioned with a given tag directly using modem_key_mgmt_cmp().
	 * Here, for the sake of the completeness, we check that a certificate exists
	 * before comparing it with what we expect it to be.
	 */
	err = modem_key_mgmt_exists(TLS_SEC_TAG, MODEM_KEY_MGMT_CRED_TYPE_CA_CHAIN, &exists);
	if (err) {
		printk("Failed to check for certificates err %d\n", err);
		return err;
	}

	if (exists) {
		mismatch = modem_key_mgmt_cmp(TLS_SEC_TAG, MODEM_KEY_MGMT_CRED_TYPE_CA_CHAIN, cert,
					      strlen(cert));
		if (!mismatch) {
			printk("Certificate match\n");
			return 0;
		}

		printk("Certificate mismatch\n");
		err = modem_key_mgmt_delete(TLS_SEC_TAG, MODEM_KEY_MGMT_CRED_TYPE_CA_CHAIN);
		if (err) {
			printk("Failed to delete existing certificate, err %d\n", err);
		}
	}

	printk("Provisioning certificate\n");

	/*  Provision certificate to the modem */
	err = modem_key_mgmt_write(TLS_SEC_TAG, MODEM_KEY_MGMT_CRED_TYPE_CA_CHAIN, cert,
				   sizeof(cert) - 1);
	if (err) {
		printk("Failed to provision certificate, err %d\n", err);
		return err;
	}

	return 0;
}

void pdn_event_handler(uint8_t cid, enum pdn_event event, int reason)
{
	switch (event) {
	case PDN_EVENT_CNEC_ESM:
		printk("PDP context %d error, %s\n", cid, pdn_esm_strerror(reason));
		break;
	case PDN_EVENT_ACTIVATED:
		printk("PDP context %d activated\n", cid);
		break;
	case PDN_EVENT_DEACTIVATED:
		printk("PDP context %d deactivated\n", cid);
		break;
	case PDN_EVENT_NETWORK_DETACH:
		printk("PDP context %d network detached\n", cid);
		break;
#if !IS_ENABLED(CONFIG_PDN_DEFAULT_FAM_IPV4)
	case PDN_EVENT_IPV6_UP:
		printk("PDP context %d IPv6 up\n", cid);
		k_sem_give(&pdn_ipv6_up_sem);
		break;
	case PDN_EVENT_IPV6_DOWN:
		printk("PDP context %d IPv6 down\n", cid);
		break;
#endif
	default:
		printk("PDP context %d, unknown event %d\n", cid, event);
		break;
	}
}

/* Setup TLS options on a given socket */
int tls_setup(int fd)
{
	int err;
	int verify;

	/* Security tag that we have provisioned the certificate with */
	const sec_tag_t tls_sec_tag[] = {
		TLS_SEC_TAG,
	};

#if defined(CONFIG_SAMPLE_TFM_MBEDTLS)
	err = tls_credential_add(tls_sec_tag[0], TLS_CREDENTIAL_CA_CERTIFICATE, cert, sizeof(cert));
	if (err) {
		return err;
	}
#endif

	/* Set up TLS peer verification */
	enum {
		NONE = 0,
		OPTIONAL = 1,
		REQUIRED = 2,
	};

	verify = REQUIRED;

	err = setsockopt(fd, SOL_TLS, TLS_PEER_VERIFY, &verify, sizeof(verify));
	if (err) {
		printk("Failed to setup peer verification, err %d\n", errno);
		return err;
	}

	/* Associate the socket with the security tag
	 * we have provisioned the certificate with.
	 */
	err = setsockopt(fd, SOL_TLS, TLS_SEC_TAG_LIST, tls_sec_tag, sizeof(tls_sec_tag));
	if (err) {
		printk("Failed to setup TLS sec tag, err %d\n", errno);
		return err;
	}

	err = setsockopt(fd, SOL_TLS, TLS_HOSTNAME, HTTPS_HOSTNAME, sizeof(HTTPS_HOSTNAME) - 1);
	if (err) {
		printk("Failed to setup TLS hostname, err %d\n", errno);
		return err;
	}
	return 0;
}

int main(void)
{
	int err;
	int fd;
	char *p;
	int bytes;
	size_t off;
	struct addrinfo *res;
	struct addrinfo hints = {
		.ai_flags = AI_NUMERICSERV, /* Let getaddrinfo() set port */
		.ai_socktype = SOCK_STREAM,
	};
	char peer_addr[INET6_ADDRSTRLEN];

	printk("HTTPS client sample started\n\r");

	err = nrf_modem_lib_init();
	if (err) {
		printk("Modem library initialization failed, error: %d\n", err);
		return 0;
	}

	/* Setup a callback for the default PDP context (zero).
	 * Do this before switching to function mode 1 (CFUN=1)
	 * to receive the first activation event.
	 */
	err = pdn_default_ctx_cb_reg(pdn_event_handler);
	if (err) {
		printk("pdn_default_ctx_cb_reg() failed, err %d\n", err);
		return 0;
	}

#if !defined(CONFIG_SAMPLE_TFM_MBEDTLS)
	/* Provision certificates before connecting to the LTE network */
	err = cert_provision();
	if (err) {
		return 0;
	}
#endif

	printk("Waiting for network.. ");
	err = lte_lc_init_and_connect();
	if (err) {
		printk("Failed to connect to the LTE network, err %d\n", err);
		return 0;
	}
	printk("OK\n");

	printk("Looking up %s\n", HTTPS_HOSTNAME);
	err = getaddrinfo(HTTPS_HOSTNAME, HTTPS_PORT, &hints, &res);
	if (err) {
		printk("getaddrinfo() failed, err %d\n", err);
		return 0;
	}

	inet_ntop(res->ai_family, &((struct sockaddr_in *)(res->ai_addr))->sin_addr, peer_addr, INET6_ADDRSTRLEN);
	printk("Resolved %s (%s)\n", peer_addr, net_family2str(res->ai_family));

	if (IS_ENABLED(CONFIG_SAMPLE_TFM_MBEDTLS)) {
		fd = socket(res->ai_family, SOCK_STREAM | SOCK_NATIVE_TLS, IPPROTO_TLS_1_2);
	} else {
		fd = socket(res->ai_family, SOCK_STREAM, IPPROTO_TLS_1_2);
	}
	if (fd == -1) {
		printk("Failed to open socket!\n");
		goto clean_up;
	}

	/* Setup TLS socket options */
	err = tls_setup(fd);
	if (err) {
		goto clean_up;
	}

	printk("Connecting to %s:%d\n", HTTPS_HOSTNAME, ntohs(((struct sockaddr_in *)(res->ai_addr))->sin_port));
	err = connect(fd, res->ai_addr, res->ai_addrlen);
	if (err) {
		printk("connect() failed, err: %d\n", errno);
		goto clean_up;
	}

	off = 0;
	do {
		bytes = send(fd, &send_buf[off], HTTP_HEAD_LEN - off, 0);
		if (bytes < 0) {
			printk("send() failed, err %d\n", errno);
			goto clean_up;
		}
		off += bytes;
	} while (off < HTTP_HEAD_LEN);

	printk("Sent %d bytes\n", off);

	off = 0;
	do {
		bytes = recv(fd, &recv_buf[off], RECV_BUF_SIZE - off, 0);
		if (bytes < 0) {
			printk("recv() failed, err %d\n", errno);
			goto clean_up;
		}
		off += bytes;
	} while (bytes != 0 /* peer closed connection */);

	printk("Received %d bytes\n", off);

	/* Make sure recv_buf is NULL terminated (for safe use with strstr) */
	if (off < sizeof(recv_buf)) {
		recv_buf[off] = '\0';
	} else {
		recv_buf[sizeof(recv_buf) - 1] = '\0';
	}

	/* Print HTTP response */
	p = strstr(recv_buf, "\r\n");
	if (p) {
		off = p - recv_buf;
		recv_buf[off + 1] = '\0';
		printk("\n>\t %s\n\n", recv_buf);
	}

	printk("Finished, closing socket.\n");

clean_up:
	freeaddrinfo(res);
	(void)close(fd);

	lte_lc_power_off();

	return 0;
}

HTTPS client sample started
Certificate match
Waiting for network.. PDP context 0 activated
PDP context 0 error, PDN type IPv4 only allowed
OK
Looking up https://login.microsoftonline.com/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/oauth2/v2.0/token
getaddrinfo() failed, err 0

How do I implement the 2 post request from my python script on the nrf9160 using Zephyr?

  • Hi,

    When calling getaddrinfo, try to use just the hostname, not the full URL.

    I.e. do the DNS lookup on just "login.microsoftonline.com".

    Best regards,

    Didrik

  • Thank you, that seems to work. I can now connect to login.microsoftonline.com and make a post request to get a bearer token, however, when I try to connect to the next hostname: log-ingestion-xxxx.westeurope-1.ingest.monitor.azure.com, i get a new error:

    *** Booting Zephyr OS build v3.3.99-ncs1 ***
    HTTPS client sample started
    Certificate match
    Waiting for network.. PDP context 0 activated
    PDP context 0 error, PDN type IPv4 only allowed
    OK
    Looking up log-ingestion-xxxx.westeurope-1.ingest.monitor.azure.com
    Resolved 51.xxx.xx.xxx (AF_INET)
    Connecting to log-ingestion-xxxx.westeurope-1.ingest.monitor.azure.com:443
    connect() failed, err: -1

    The connection works perfectly with the other hostname, so why does it fail here? And what does error code -1 mean?

  • You need to print the value of errno to see the actual error code from connect.

    My guess is that you have something wrong with the certificates.

    Also, when debugging these kinds lf issues, a modem trace can be very useful. With the Cellular Monitor application in nRF Connect for Dekstop, you can decode the trace so that you can see that packets sent on air in Wireshark. This will e.g. let you see where in the TLS handshake the problem occurs.

    https://docs.nordicsemi.com/bundle/nrf-connect-cellularmonitor/page/requirements.html

  • Thanks, you were right it was the certificate that was wrong. I can now make a post request to both hostnames, however, the sample uses the "connection: close" which closes the connection after 1 post request. Is there a way to not close the connection after sending a request? I've tried "keep-alive" but the sample relies on closing the connection when receiving data, so you have to receive data in another way?

    off = 0;
    do {
    	bytes = recv(fd, &recv_buf[off], 2100 - off, 0);
    	if (bytes < 0) {
    		printk("recv() failed, err %d\n", errno);
    		k_sleep(K_MSEC(1000));
    	}
    	off += bytes;
    } while (bytes != 0 /* peer closed connection */);

  • Or even better, how do I open the connection again after it is closed? I get error 128 when I try to connect again using the same fd:

    printk("Connecting to %s:%d\n", HTTPS_HOSTNAME, ntohs(((struct sockaddr_in *)(res->ai_addr))->sin_port));
    err = connect(fd, res->ai_addr, res->ai_addrlen);
    if (err) {
    	printk("connect() failed, err: %d\n", errno);
    }

Related