mirror of
https://github.com/warmcat/libwebsockets.git
synced 2025-03-09 00:00:04 +01:00
ss: policy: add auth mapping section
This commit is contained in:
parent
ce3b99cd8f
commit
940abe557a
9 changed files with 210 additions and 23 deletions
|
@ -178,6 +178,20 @@ typedef struct lws_ss_http_respmap {
|
|||
uint16_t state; /* low 16-bits of associated state */
|
||||
} lws_ss_http_respmap_t;
|
||||
|
||||
/*
|
||||
* This is a mapping between an auth streamtype and a name and other information
|
||||
* that can be independently instantiated. Other streamtypes can indicate they
|
||||
* require this authentication on their connection.
|
||||
*/
|
||||
|
||||
typedef struct lws_ss_auth {
|
||||
struct lws_ss_auth *next;
|
||||
const char *name;
|
||||
|
||||
const char *streamtype;
|
||||
uint8_t blob_index;
|
||||
} lws_ss_auth_t;
|
||||
|
||||
/**
|
||||
* lws_ss_policy_t: policy database entry for a stream type
|
||||
*
|
||||
|
@ -202,6 +216,7 @@ typedef struct lws_ss_policy {
|
|||
const char *payload_fmt;
|
||||
const char *socks5_proxy;
|
||||
lws_ss_metadata_t *metadata; /* linked-list of metadata */
|
||||
const lws_ss_auth_t *auth; /* NULL or auth object we bind to */
|
||||
|
||||
/* protocol-specific connection policy details */
|
||||
|
||||
|
@ -326,10 +341,14 @@ LWS_VISIBLE LWS_EXTERN int
|
|||
lws_ss_policy_overlay(struct lws_context *context, const char *overlay);
|
||||
|
||||
/*
|
||||
* You almost certainly don't want this, it returns the first policy object
|
||||
* in a linked-list of objects created by lws_ss_policy_parse above
|
||||
* You almost certainly don't want these, they return the first policy or auth
|
||||
* object in a linked-list of objects created by lws_ss_policy_parse above,
|
||||
* they are exported to generate static policy with
|
||||
*/
|
||||
LWS_VISIBLE LWS_EXTERN const lws_ss_policy_t *
|
||||
lws_ss_policy_get(struct lws_context *context);
|
||||
|
||||
LWS_VISIBLE LWS_EXTERN const lws_ss_auth_t *
|
||||
lws_ss_auth_get(struct lws_context *context);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -554,6 +554,7 @@ struct lws_context {
|
|||
void *pol_args;
|
||||
#endif
|
||||
const lws_ss_policy_t *pss_policies;
|
||||
const lws_ss_auth_t *pss_auths;
|
||||
#if defined(LWS_WITH_SSPLUGINS)
|
||||
const lws_ss_plugin_t **pss_plugins;
|
||||
#endif
|
||||
|
|
|
@ -210,6 +210,34 @@ with one of these to enforce validity checking of the remote server.
|
|||
|
||||
Entries should be named using "name" and the stack array defined using "stack"
|
||||
|
||||
### `auth`
|
||||
|
||||
Optional section describing a map of available authentication streamtypes to
|
||||
auth token blob indexes.
|
||||
|
||||
```
|
||||
...
|
||||
"auth": [{"name":"lwa","streamtype":"api_amazon_com_lwa","blob":0}]
|
||||
...
|
||||
```
|
||||
|
||||
Streams can indicate they depend on a valid auth token from one of these schemes
|
||||
by using the `"use_auth": "name"` member in the streamtype definition, where name
|
||||
is, eg, "lwa" in the example above.
|
||||
|
||||
### `auth[].name`
|
||||
|
||||
This is the name of the authentication scheme used by other streamtypes
|
||||
|
||||
### `auth[].streamtype`
|
||||
|
||||
This is the auth streamtype to be used to refresh the authentication token
|
||||
|
||||
### `auth[].blob`
|
||||
|
||||
This is the auth blob index the authentication token is stored into and retreived
|
||||
from
|
||||
|
||||
### `s`
|
||||
|
||||
These are an array of policies for the supported stream type names.
|
||||
|
@ -308,6 +336,11 @@ policy is applied.
|
|||
The name of the trust store described in the `trust_stores` section to apply
|
||||
to validate the remote server cert.
|
||||
|
||||
### `use_auth`
|
||||
|
||||
Indicate that the streamtype should use the named auth type from the `auth`
|
||||
array in the policy
|
||||
|
||||
### `server_cert`
|
||||
|
||||
**SERVER ONLY**: subject to change... the name of the x.509 cert that is the
|
||||
|
|
|
@ -100,7 +100,12 @@ static const char * const lejp_tokens_policy[] = {
|
|||
"s[].*.mqtt_will_qos",
|
||||
"s[].*.mqtt_will_retain",
|
||||
"s[].*.swake_validity",
|
||||
"s[].*.use_auth",
|
||||
"s[].*",
|
||||
"auth[].name",
|
||||
"auth[].streamtype",
|
||||
"auth[].blob",
|
||||
"auth[]",
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
|
@ -175,7 +180,12 @@ typedef enum {
|
|||
LSSPPT_MQTT_WILL_QOS,
|
||||
LSSPPT_MQTT_WILL_RETAIN,
|
||||
LSSPPT_SWAKE_VALIDITY,
|
||||
LSSPPT_USE_AUTH,
|
||||
LSSPPT_STREAMTYPES,
|
||||
LSSPPT_AUTH_NAME,
|
||||
LSSPPT_AUTH_STREAMTYPE,
|
||||
LSSPPT_AUTH_BLOB,
|
||||
LSSPPT_AUTH,
|
||||
|
||||
} policy_token_t;
|
||||
|
||||
|
@ -188,9 +198,10 @@ static uint8_t sizes[] = {
|
|||
sizeof(lws_ss_x509_t),
|
||||
sizeof(lws_ss_trust_store_t),
|
||||
sizeof(lws_ss_policy_t),
|
||||
sizeof(lws_ss_auth_t),
|
||||
};
|
||||
|
||||
static const char *protonames[] = {
|
||||
static const char * const protonames[] = {
|
||||
"h1", /* LWSSSP_H1 */
|
||||
"h2", /* LWSSSP_H2 */
|
||||
"ws", /* LWSSSP_WS */
|
||||
|
@ -198,6 +209,24 @@ static const char *protonames[] = {
|
|||
"raw", /* LWSSSP_RAW */
|
||||
};
|
||||
|
||||
static const lws_ss_auth_t *
|
||||
lws_ss_policy_find_auth_by_name(struct policy_cb_args *a,
|
||||
const char *name, size_t len)
|
||||
{
|
||||
const lws_ss_auth_t *auth = a->heads[LTY_AUTH].a;
|
||||
|
||||
while (auth) {
|
||||
if (auth->name &&
|
||||
len == strlen(auth->name) &&
|
||||
!strncmp(auth->name, name, len))
|
||||
return auth;
|
||||
|
||||
auth = auth->next;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static signed char
|
||||
lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
|
||||
{
|
||||
|
@ -216,8 +245,8 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
|
|||
backoff_t *bot;
|
||||
int n = -1;
|
||||
|
||||
lwsl_debug("%s: %d %d %s\n", __func__, reason, ctx->path_match - 1,
|
||||
ctx->path);
|
||||
// lwsl_debug("%s: %d %d %s\n", __func__, reason, ctx->path_match - 1,
|
||||
// ctx->path);
|
||||
|
||||
switch (ctx->path_match - 1) {
|
||||
case LSSPPT_RETRY:
|
||||
|
@ -233,6 +262,9 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
|
|||
case LSSPPT_STREAMTYPES:
|
||||
n = LTY_POLICY;
|
||||
break;
|
||||
case LSSPPT_AUTH:
|
||||
n = LTY_AUTH;
|
||||
break;
|
||||
}
|
||||
|
||||
if (reason == LEJPCB_ARRAY_START &&
|
||||
|
@ -241,6 +273,17 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
|
|||
ctx->path_match - 1 == LSSPPT_HTTPRESPMAP))
|
||||
a->count = 0;
|
||||
|
||||
if (reason == LEJPCB_OBJECT_START && n == LTY_AUTH) {
|
||||
a->curr[n].b = lwsac_use_zero(&a->ac, sizes[n], POL_AC_GRAIN);
|
||||
if (!a->curr[n].b)
|
||||
goto oom;
|
||||
|
||||
a->curr[n].b->next = a->heads[n].b;
|
||||
a->heads[n].b = a->curr[n].b;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (reason == LEJPCB_ARRAY_END &&
|
||||
ctx->path_match - 1 == LSSPPT_TRUST_STORES_STACK && !a->count) {
|
||||
lwsl_err("%s: at least one cert required in trust store\n",
|
||||
|
@ -290,7 +333,8 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (reason == LEJPCB_PAIR_NAME && n != -1 && n != LTY_TRUSTSTORE) {
|
||||
if (reason == LEJPCB_PAIR_NAME && n != -1 &&
|
||||
(n != LTY_TRUSTSTORE && n != LTY_AUTH)) {
|
||||
|
||||
p2 = NULL;
|
||||
if (n == LTY_POLICY) {
|
||||
|
@ -574,6 +618,10 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
|
|||
a->curr[LTY_POLICY].p->client_cert = (uint8_t)(atoi(ctx->buf) + 1);
|
||||
break;
|
||||
|
||||
case LSSPPT_AUTH_BLOB:
|
||||
a->curr[LTY_AUTH].a->blob_index = (uint8_t)atoi(ctx->buf);
|
||||
break;
|
||||
|
||||
case LSSPPT_HTTP_EXPECT:
|
||||
a->curr[LTY_POLICY].p->u.http.resp_expect = (uint16_t)atoi(ctx->buf);
|
||||
break;
|
||||
|
@ -655,6 +703,16 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
|
|||
case LSSPPT_METADATA:
|
||||
break;
|
||||
|
||||
case LSSPPT_USE_AUTH:
|
||||
a->curr[LTY_POLICY].p->auth =
|
||||
lws_ss_policy_find_auth_by_name(a, ctx->buf, ctx->npos);
|
||||
if (!a->curr[LTY_POLICY].p->auth) {
|
||||
lws_strnncpy(dotstar, ctx->buf, ctx->npos, sizeof(dotstar));
|
||||
lwsl_err("%s: unknown auth '%s'\n", __func__, dotstar);
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
|
||||
case LSSPPT_METADATA_ITEM:
|
||||
pmd = a->curr[LTY_POLICY].p->metadata;
|
||||
a->curr[LTY_POLICY].p->metadata = lwsac_use_zero(&a->ac,
|
||||
|
@ -744,6 +802,14 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
|
|||
pp = (char **)&a->curr[LTY_POLICY].p->u.http.multipart_content_type;
|
||||
goto string2;
|
||||
|
||||
case LSSPPT_AUTH_NAME:
|
||||
pp = (char **)&a->curr[LTY_AUTH].a->name;
|
||||
goto string2;
|
||||
|
||||
case LSSPPT_AUTH_STREAMTYPE:
|
||||
pp = (char **)&a->curr[LTY_AUTH].a->streamtype;
|
||||
goto string2;
|
||||
|
||||
case LSSPPT_HTTP_FAIL_REDIRECT:
|
||||
a->curr[LTY_POLICY].p->u.http.fail_redirect =
|
||||
reason == LEJPCB_VAL_TRUE;
|
||||
|
@ -950,3 +1016,14 @@ lws_ss_policy_get(struct lws_context *context)
|
|||
|
||||
return args->heads[LTY_POLICY].p;
|
||||
}
|
||||
|
||||
const lws_ss_auth_t *
|
||||
lws_ss_auth_get(struct lws_context *context)
|
||||
{
|
||||
struct policy_cb_args *args = (struct policy_cb_args *)context->pol_args;
|
||||
|
||||
if (!args)
|
||||
return NULL;
|
||||
|
||||
return args->heads[LTY_AUTH].a;
|
||||
}
|
||||
|
|
|
@ -312,10 +312,11 @@ typedef struct backoffs {
|
|||
} backoff_t;
|
||||
|
||||
union u {
|
||||
backoff_t *b;
|
||||
lws_ss_x509_t *x;
|
||||
lws_ss_trust_store_t *t;
|
||||
lws_ss_policy_t *p;
|
||||
backoff_t *b;
|
||||
lws_ss_x509_t *x;
|
||||
lws_ss_trust_store_t *t;
|
||||
lws_ss_policy_t *p;
|
||||
lws_ss_auth_t *a;
|
||||
};
|
||||
|
||||
enum {
|
||||
|
@ -323,6 +324,7 @@ enum {
|
|||
LTY_X509,
|
||||
LTY_TRUSTSTORE,
|
||||
LTY_POLICY,
|
||||
LTY_AUTH,
|
||||
|
||||
_LTY_COUNT /* always last */
|
||||
};
|
||||
|
|
|
@ -175,14 +175,20 @@ static const char * const default_ss_policy =
|
|||
"]"
|
||||
"}"
|
||||
"],"
|
||||
"\"auth\": [" /* available auth type bindings */
|
||||
"{"
|
||||
"\"name\":" "\"lwa\","
|
||||
"\"streamtype\":" "\"api_amazon_com_lwa\","
|
||||
"\"blob\":" "0"
|
||||
"}"
|
||||
"],"
|
||||
"\"s\": [" /* the supported stream types */
|
||||
"{\"api_amazon_com_auth\": {"
|
||||
"{\"api_amazon_com_lwa\": {"
|
||||
"\"endpoint\":" "\"api.amazon.com\","
|
||||
"\"port\":" "443,"
|
||||
"\"protocol\":" "\"h1\","
|
||||
"\"http_method\":" "\"POST\","
|
||||
"\"http_url\":" "\"auth/o2/token\","
|
||||
"\"plugins\":" "[],"
|
||||
"\"opportunistic\":" "true,"
|
||||
"\"tls\":" "true,"
|
||||
"\"h2q_oflow_txcr\":" "true,"
|
||||
|
@ -200,6 +206,7 @@ static const char * const default_ss_policy =
|
|||
"\"protocol\":" "\"h2\","
|
||||
"\"http_method\":" "\"GET\","
|
||||
"\"http_url\":" "\"v20160207/directives\","
|
||||
"\"use_auth\":" "\"lwa\","
|
||||
"\"h2q_oflow_txcr\":" "true,"
|
||||
"\"http_auth_header\":" "\"authorization:\","
|
||||
"\"http_auth_preamble\":" "\"Bearer \","
|
||||
|
@ -207,7 +214,6 @@ static const char * const default_ss_policy =
|
|||
"\"nailed_up\":" "true,"
|
||||
"\"long_poll\":" "true,"
|
||||
"\"retry\":" "\"default\","
|
||||
"\"plugins\":" "[],"
|
||||
"\"tls\":" "true,"
|
||||
"\"tls_trust_store\":" "\"avs_via_starfield\""
|
||||
"}},"
|
||||
|
@ -223,6 +229,7 @@ static const char * const default_ss_policy =
|
|||
"\"protocol\":" "\"h2\","
|
||||
"\"http_method\":" "\"POST\","
|
||||
"\"http_url\":" "\"v20160207/events\","
|
||||
"\"use_auth\":" "\"lwa\","
|
||||
"\"opportunistic\":" "true,"
|
||||
"\"h2q_oflow_txcr\":" "true,"
|
||||
"\"http_auth_header\":" "\"authorization:\","
|
||||
|
@ -233,7 +240,6 @@ static const char * const default_ss_policy =
|
|||
"\"http_multipart_ss_in\":" "true,"
|
||||
"\"rideshare\":" "\"avs_audio\","
|
||||
"\"retry\":" "\"default\","
|
||||
"\"plugins\":" "[],"
|
||||
"\"tls\":" "true,"
|
||||
"\"tls_trust_store\":" "\"avs_via_starfield\""
|
||||
"}},"
|
||||
|
@ -243,7 +249,7 @@ static const char * const default_ss_policy =
|
|||
"\"protocol\":" "\"h2\","
|
||||
"\"http_method\":" "\"POST\","
|
||||
"\"http_url\":" "\"v20160207/events\","
|
||||
"\"plugins\":" "[],"
|
||||
"\"use_auth\":" "\"lwa\","
|
||||
"\"tls\":" "true,"
|
||||
"\"h2q_oflow_txcr\":" "true,"
|
||||
"\"http_auth_header\":" "\"authorization:\","
|
||||
|
|
|
@ -151,14 +151,20 @@ static const char * const default_ss_policy =
|
|||
"]"
|
||||
"}"
|
||||
"],"
|
||||
"\"auth\": [" /* available auth type bindings */
|
||||
"{"
|
||||
"\"name\":" "\"lwa\","
|
||||
"\"streamtype\":" "\"api_amazon_com_lwa\","
|
||||
"\"blob\":" "0"
|
||||
"}"
|
||||
"],"
|
||||
"\"s\": [" /* the supported stream types */
|
||||
"{\"api_amazon_com_auth\": {"
|
||||
"{\"api_amazon_com_lwa\": {"
|
||||
"\"endpoint\":" "\"api.amazon.com\","
|
||||
"\"port\":" "443,"
|
||||
"\"protocol\":" "\"h1\","
|
||||
"\"http_method\":" "\"POST\","
|
||||
"\"http_url\":" "\"auth/o2/token\","
|
||||
"\"plugins\":" "[],"
|
||||
"\"opportunistic\":" "true,"
|
||||
"\"tls\":" "true,"
|
||||
"\"h2q_oflow_txcr\":" "true,"
|
||||
|
@ -176,10 +182,10 @@ static const char * const default_ss_policy =
|
|||
"\"h2q_oflow_txcr\":" "true,"
|
||||
"\"http_auth_header\":" "\"authorization:\","
|
||||
"\"http_auth_preamble\":" "\"Bearer \","
|
||||
"\"use_auth\":" "\"lwa\","
|
||||
"\"nailed_up\":" "true,"
|
||||
"\"long_poll\":" "true,"
|
||||
"\"retry\":" "\"default\","
|
||||
"\"plugins\":" "[],"
|
||||
"\"tls\":" "true,"
|
||||
"\"tls_trust_store\":" "\"avs_via_starfield\""
|
||||
"}},"
|
||||
|
@ -191,6 +197,7 @@ static const char * const default_ss_policy =
|
|||
"\"http_url\":" "\"v20160207/events\","
|
||||
"\"http_no_content_length\":" "true,"
|
||||
"\"h2q_oflow_txcr\":" "true,"
|
||||
"\"use_auth\":" "\"lwa\","
|
||||
"\"http_auth_header\":" "\"authorization:\","
|
||||
"\"http_auth_preamble\":" "\"Bearer \","
|
||||
"\"http_multipart_name\":" "\"metadata\","
|
||||
|
@ -200,7 +207,6 @@ static const char * const default_ss_policy =
|
|||
#endif
|
||||
"\"rideshare\":" "\"avs_audio\","
|
||||
"\"retry\":" "\"default\","
|
||||
"\"plugins\":" "[],"
|
||||
"\"tls\":" "true,"
|
||||
"\"tls_trust_store\":" "\"avs_via_starfield\""
|
||||
"}},"
|
||||
|
@ -211,12 +217,12 @@ static const char * const default_ss_policy =
|
|||
"\"http_method\":" "\"POST\","
|
||||
"\"http_url\":" "\"v20160207/events\","
|
||||
"\"http_no_content_length\":" "true,"
|
||||
"\"plugins\":" "[],"
|
||||
"\"tls\":" "true,"
|
||||
"\"h2q_oflow_txcr\":" "true,"
|
||||
#if 1
|
||||
"\"http_multipart_ss_in\":" "true,"
|
||||
#endif
|
||||
"\"use_auth\":" "\"lwa\","
|
||||
"\"http_auth_header\":" "\"authorization:\","
|
||||
"\"http_auth_preamble\":" "\"Bearer \","
|
||||
"\"http_multipart_name\":" "\"audio\","
|
||||
|
@ -345,6 +351,8 @@ int main(int argc, const char **argv)
|
|||
nl.notify_cb = app_system_state_nf;
|
||||
info.register_notifier_list = app_notifier_list;
|
||||
|
||||
puts(default_ss_policy);
|
||||
|
||||
context = lws_create_context(&info);
|
||||
if (!context) {
|
||||
lwsl_err("lws init failed\n");
|
||||
|
|
|
@ -74,6 +74,7 @@ int main(int argc, const char **argv)
|
|||
struct lws_context_creation_info info;
|
||||
size_t json_size = 0, est = 0;
|
||||
struct lws_context *context;
|
||||
const lws_ss_auth_t *auth;
|
||||
char prev[128], curr[128];
|
||||
int unique_rbo = 0, m, n;
|
||||
char buf[64], buf1[64];
|
||||
|
@ -362,6 +363,7 @@ int main(int argc, const char **argv)
|
|||
pol = pol->next;
|
||||
}
|
||||
|
||||
|
||||
/* dump any streamtype's http resp map */
|
||||
|
||||
pol = lws_ss_policy_get(context);
|
||||
|
@ -406,6 +408,41 @@ int main(int argc, const char **argv)
|
|||
if (m)
|
||||
printf(";\n");
|
||||
|
||||
/*
|
||||
* The auth map
|
||||
*/
|
||||
|
||||
auth = lws_ss_auth_get(context);
|
||||
if (auth)
|
||||
printf("\nstatic const lws_ss_auth_t ");
|
||||
prev[0] = '\0';
|
||||
|
||||
while (auth) {
|
||||
lws_snprintf(curr, sizeof(curr), "_ssau_%s",
|
||||
purify_csymbol(auth->name, buf, sizeof(buf)));
|
||||
|
||||
printf("%s = {\n", curr);
|
||||
if (prev[0])
|
||||
printf("\t.next = (void *)&%s,\n", prev);
|
||||
|
||||
printf("\t.name = \"%s\",\n", auth->name);
|
||||
printf("\t.streamtype = \"%s\",\n", auth->streamtype);
|
||||
printf("\t.blob = %d,\n", auth->blob_index);
|
||||
printf("}");
|
||||
if (auth->next)
|
||||
printf(",");
|
||||
else
|
||||
printf(";");
|
||||
printf("\n");
|
||||
|
||||
lws_strncpy(prev, curr, sizeof(prev));
|
||||
|
||||
auth = auth->next;
|
||||
}
|
||||
|
||||
if (lws_ss_auth_get(context))
|
||||
printf("\n");
|
||||
|
||||
/*
|
||||
* The streamtypes
|
||||
*/
|
||||
|
@ -440,6 +477,10 @@ int main(int argc, const char **argv)
|
|||
printf("\t.socks5_proxy = \"%s\",\n",
|
||||
pol->socks5_proxy);
|
||||
|
||||
if (pol->auth)
|
||||
printf("\t.auth = &_ssau_%s,\n",
|
||||
purify_csymbol(pol->auth->name, buf, sizeof(buf)));
|
||||
|
||||
{
|
||||
lws_ss_metadata_t *nv = pol->metadata, *last = NULL;
|
||||
|
||||
|
|
|
@ -1006,19 +1006,19 @@ static const lws_ss_trust_store_t _ss_ts_le_via_isrg = {
|
|||
|
||||
static const lws_ss_metadata_t _md_mintest_xctype = {
|
||||
.name = "xctype",
|
||||
.value = (void *)"X-Content-Type:",
|
||||
.value__may_own_heap = (void *)"X-Content-Type:",
|
||||
.length = 0,
|
||||
},
|
||||
_md_mintest_ctype = {
|
||||
.next = (void *)&_md_mintest_xctype,
|
||||
.name = "ctype",
|
||||
.value = (void *)"Content-Type:",
|
||||
.value__may_own_heap = (void *)"Content-Type:",
|
||||
.length = 1,
|
||||
},
|
||||
_md_mintest_uptag = {
|
||||
.next = (void *)&_md_mintest_ctype,
|
||||
.name = "uptag",
|
||||
.value = (void *)"X-Upload-Tag:",
|
||||
.value__may_own_heap = (void *)"X-Upload-Tag:",
|
||||
.length = 2,
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue