From 7d6566272cdd4a5fff9019827832620a7a2076f0 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 22 Feb 2023 19:13:08 -0800 Subject: [PATCH] XML: added XMLNode API to modify XML documents. - delete node.$attr$a is a shorthand syntax for node.removeAttribute('a') - node.removeAllAttributes() removes all attributes of the node. - node.removeAttribute(attr_name) removes attribute named attr_name. - node.setAttribute(attr_name, value || null) sets a value for an attr_name. When value is null attribute named attr_name is deleted. - node.$attr$a = 'xxx' is a shorthand syntax for node.setAttribute('a', 'xxx'); - The methods and operations below make copy-on-write changes to the original XML parsed document. For example: var doc = xml.parse(); var b = doc.$root.a.b; doc.$root.removeAllChildren(); console.log((new TextDecoder()).decode(xml.c14n(doc))); /* */ console.log(b); /* XMLNode {$name:'b'} */ console.log(b.$parent); /* XMLNode {$name:'a',$tags:[XMLNode {$name:'b'}] */ "b" is valid after removeAllChildren() call, but is not a part of the document tree anymore. - node.addChild(nd) adds XMLNode as a child to node. nd recursively copied before adding to the node. - node.removeChildren(tag_name?) removes all the children tags named tag_name. If tag_name is absent all children tags are removed. - node.removeText() removes the node's text value. - node.setText(string || null) sets a text value for the node. When value is null the node's text is deleted. - node.$tags = [node1, node2, ..] is a shorthand syntax for node.removeChildren(); node.addChild(node1); node.addChild(node2) - node.$text = 'xxx' is a shorthand syntax for node.setText('xxx'); In addition the following method were added: - xml.serialize() is the same as xml.c14n() - xml.serializeToString() is the same as xml.c14n() except it returns the result as string. Example: const xml = require("xml"); let data = `ToveJani`; let doc = xml.parse(data); doc.$root.to.$attr$b = 'bar2'; doc.$root.to.setAttribute('c', 'baz'); delete doc.$root.to.$attr$a; console.log(xml.serializeToString(doc.$root.to)) /* 'Tove' */ doc.$root.to.removeAllAttributes(); doc.$root.from.$text = 'Jani2'; console.log(xml.serializeToString(doc)) /* 'ToveJani2' */ doc.$root.to.$tags = [xml.parse(``), xml.parse(``)]; doc.$root.to.addChild(xml.parse(``)); console.log(xml.serializeToString(doc.$root.to)) /* '' */ doc.$root.to.removeChildren('a'); console.log(xml.serializeToString(doc.$root.to)) /* '' */ --- external/njs_xml_module.c | 1140 ++++++++++++++++++++++++++++++------- src/test/njs_unit_test.c | 203 ++++++- test/xml/saml_verify.t.js | 105 +++- ts/njs_modules/xml.d.ts | 42 +- 4 files changed, 1259 insertions(+), 231 deletions(-) diff --git a/external/njs_xml_module.c b/external/njs_xml_module.c index b0bae907..b8203cce 100644 --- a/external/njs_xml_module.c +++ b/external/njs_xml_module.c @@ -39,12 +39,11 @@ struct njs_xml_nset_s { static njs_int_t njs_xml_ext_parse(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused); static njs_int_t njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, - njs_uint_t nargs, njs_index_t unused); + njs_uint_t nargs, njs_index_t magic); static njs_int_t njs_xml_doc_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys); static njs_int_t njs_xml_doc_ext_root(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *unused, njs_value_t *retval); -static void njs_xml_doc_cleanup(void *data); static njs_int_t njs_xml_node_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys); static njs_int_t njs_xml_node_ext_prop_handler(njs_vm_t *vm, @@ -55,6 +54,8 @@ static njs_int_t njs_xml_attr_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, static njs_int_t njs_xml_attr_ext_prop_handler(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *unused, njs_value_t *retval); +static njs_int_t njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused); static njs_int_t njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); static njs_int_t njs_xml_node_ext_name(njs_vm_t *vm, njs_object_prop_t *prop, @@ -63,11 +64,44 @@ static njs_int_t njs_xml_node_ext_ns(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); static njs_int_t njs_xml_node_ext_parent(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); +static njs_int_t njs_xml_node_ext_remove_all_attributes(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); +static njs_int_t njs_xml_node_ext_remove_attribute(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); +static njs_int_t njs_xml_node_ext_remove_children(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); +static njs_int_t njs_xml_node_ext_remove_text(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); +static njs_int_t njs_xml_node_ext_set_attribute(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); +static njs_int_t njs_xml_node_ext_set_text(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); static njs_int_t njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); static njs_int_t njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); +static njs_int_t njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current, + njs_str_t *name, njs_value_t *setval, njs_value_t *retval); +static njs_int_t njs_xml_node_tag_remove(njs_vm_t *vm, xmlNode *current, + njs_str_t *name); +static njs_int_t njs_xml_node_tag_handler(njs_vm_t *vm, xmlNode *current, + njs_str_t *name, njs_value_t *setval, njs_value_t *retval); +static njs_int_t njs_xml_node_tags_handler(njs_vm_t *vm, xmlNode *current, + njs_str_t *name, njs_value_t *setval, njs_value_t *retval); + +static xmlNode *njs_xml_external_node(njs_vm_t *vm, njs_value_t *value); +static njs_int_t njs_xml_str_to_c_string(njs_vm_t *vm, njs_str_t *str, + u_char *dst, size_t size); +static const u_char *njs_xml_value_to_c_string(njs_vm_t *vm, njs_value_t *value, + u_char *dst, size_t size); +static njs_int_t njs_xml_encode_special_chars(njs_vm_t *vm, njs_str_t *src, + njs_str_t *out); +static njs_int_t njs_xml_replace_node(njs_vm_t *vm, xmlNode *old, + xmlNode *current); +static void njs_xml_node_cleanup(void *data); +static void njs_xml_doc_cleanup(void *data); + static njs_xml_nset_t *njs_xml_nset_create(njs_vm_t *vm, xmlDoc *doc, xmlNode *current, njs_xml_nset_type_t type); static njs_xml_nset_t *njs_xml_nset_add(njs_xml_nset_t *nset, @@ -122,6 +156,29 @@ static njs_external_t njs_ext_xml[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("serialize"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_ext_canonicalization, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("serializeToString"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_ext_canonicalization, + .magic8 = 3, + } + }, + }; @@ -171,11 +228,24 @@ static njs_external_t njs_ext_xml_node[] = { .flags = NJS_EXTERN_SELF, .u.object = { .enumerable = 1, + .writable = 1, + .configurable = 1, .prop_handler = njs_xml_node_ext_prop_handler, .keys = njs_xml_node_ext_prop_keys, } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("addChild"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_add_child, + } + }, + { .flags = NJS_EXTERN_PROPERTY, .name.string = njs_str("$attrs"), @@ -212,10 +282,78 @@ static njs_external_t njs_ext_xml_node[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("removeAllAttributes"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_remove_all_attributes, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("removeAttribute"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_remove_attribute, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("removeChildren"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_remove_children, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("removeText"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_remove_text, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("setAttribute"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_set_attribute, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("setText"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_set_text, + } + }, + { .flags = NJS_EXTERN_PROPERTY, .name.string = njs_str("$tags"), .enumerable = 1, + .writable = 1, + .configurable = 1, .u.property = { .handler = njs_xml_node_ext_tags, } @@ -225,6 +363,7 @@ static njs_external_t njs_ext_xml_node[] = { .flags = NJS_EXTERN_PROPERTY, .name.string = njs_str("$text"), .enumerable = 1, + .writable = 1, .u.property = { .handler = njs_xml_node_ext_text, } @@ -410,16 +549,6 @@ njs_xml_doc_ext_root(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, } -static void -njs_xml_doc_cleanup(void *data) -{ - njs_xml_doc_t *current = data; - - xmlFreeDoc(current->doc); - xmlFreeParserCtxt(current->ctx); -} - - static njs_int_t njs_xml_node_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys) { @@ -515,15 +644,11 @@ njs_xml_node_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys) static njs_int_t njs_xml_node_ext_prop_handler(njs_vm_t *vm, njs_object_prop_t *prop, - njs_value_t *value, njs_value_t *unused, njs_value_t *retval) + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) { - size_t size; - xmlAttr *attr; - xmlNode *node, *current; - njs_int_t ret; - njs_str_t name; - njs_value_t *push; - const u_char *content; + xmlNode *current; + njs_int_t ret; + njs_str_t name; /* * $tag$foo - the first tag child with the name "foo" @@ -548,108 +673,93 @@ njs_xml_node_ext_prop_handler(njs_vm_t *vm, njs_object_prop_t *prop, if (name.length > njs_length("$attr$") && njs_strncmp(&name.start[1], "attr$", njs_length("attr$")) == 0) { - for (attr = current->properties; attr != NULL; attr = attr->next) { - if (attr->type != XML_ATTRIBUTE_NODE) { - continue; - } - - size = njs_strlen(attr->name); - - if (name.length != (size + njs_length("$attr$")) - || njs_strncmp(&name.start[njs_length("$attr$")], - attr->name, size) != 0) - { - continue; - } - - content = (const u_char *) attr->children->content; + name.length -= njs_length("$attr$"); + name.start += njs_length("$attr$"); - return njs_vm_value_string_create(vm, retval, content, - njs_strlen(content)); - } + return njs_xml_node_attr_handler(vm, current, &name, setval, + retval); } if (name.length > njs_length("$tag$") && njs_strncmp(&name.start[1], "tag$", njs_length("tag$")) == 0) { - for (node = current->children; node != NULL; node = node->next) { - if (node->type != XML_ELEMENT_NODE) { - continue; - } - - size = njs_strlen(node->name); - - if (name.length != (size + njs_length("$tag$")) - || njs_strncmp(&name.start[njs_length("$tag$")], - node->name, size) != 0) - { - continue; - } - - return njs_vm_external_create(vm, retval, njs_xml_node_proto_id, - node, 0); - } + name.length -= njs_length("$tag$"); + name.start += njs_length("$tag$"); + + return njs_xml_node_tag_handler(vm, current, &name, setval, retval); } if (name.length >= njs_length("$tags$") && njs_strncmp(&name.start[1], "tags$", njs_length("tags$")) == 0) { - ret = njs_vm_array_alloc(vm, retval, 2); - if (njs_slow_path(ret != NJS_OK)) { - return NJS_ERROR; - } + name.length -= njs_length("$tags$"); + name.start += njs_length("$tags$"); - for (node = current->children; node != NULL; node = node->next) { - if (node->type != XML_ELEMENT_NODE) { - continue; - } - - size = njs_strlen(node->name); - - if (name.length > njs_length("$tags$") - && (name.length != (size + njs_length("$tags$")) - || njs_strncmp(&name.start[njs_length("$tags$")], - node->name, size) != 0)) - { - continue; - } - - push = njs_vm_array_push(vm, retval); - if (njs_slow_path(push == NULL)) { - return NJS_ERROR; - } - - ret = njs_vm_external_create(vm, push, njs_xml_node_proto_id, - node, 0); - if (njs_slow_path(ret != NJS_OK)) { - return NJS_ERROR; - } - } - - return NJS_OK; + return njs_xml_node_tags_handler(vm, current, &name, setval, + retval); } } - for (node = current->children; node != NULL; node = node->next) { - if (node->type != XML_ELEMENT_NODE) { - continue; - } + return njs_xml_node_tag_handler(vm, current, &name, setval, retval); +} - size = njs_strlen(node->name); - if (name.length != size - || njs_strncmp(name.start, node->name, size) != 0) - { - continue; - } +static njs_int_t +njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) +{ + xmlNode *current, *node, *copy; + njs_int_t ret; - return njs_vm_external_create(vm, retval, njs_xml_node_proto_id, - node, 0); + current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0)); + if (njs_slow_path(current == NULL)) { + njs_vm_error(vm, "\"this\" is not a XMLNode object"); + return NJS_ERROR; } - njs_value_undefined_set(retval); + copy = xmlDocCopyNode(current, current->doc, 1); + if (njs_slow_path(copy == NULL)) { + njs_vm_error(vm, "xmlDocCopyNode() failed"); + return NJS_ERROR; + } - return NJS_DECLINED; + node = njs_xml_external_node(vm, njs_arg(args, nargs, 1)); + if (njs_slow_path(node == NULL)) { + njs_vm_error(vm, "node is not a XMLNode object"); + goto error; + } + + node = xmlDocCopyNode(node, current->doc, 1); + if (njs_slow_path(node == NULL)) { + njs_vm_error(vm, "xmlDocCopyNode() failed"); + goto error; + } + + node = xmlAddChild(copy, node); + if (njs_slow_path(node == NULL)) { + njs_vm_error(vm, "xmlAddChild() failed"); + goto error; + } + + ret = xmlReconciliateNs(current->doc, copy); + if (njs_slow_path(ret == -1)) { + njs_vm_error(vm, "xmlReconciliateNs() failed"); + return NJS_ERROR; + } + + njs_value_undefined_set(njs_vm_retval(vm)); + + return njs_xml_replace_node(vm, current, copy); + +error: + + if (node != NULL) { + xmlFreeNode(node); + } + + xmlFreeNode(copy); + + return NJS_ERROR; } @@ -725,121 +835,755 @@ njs_xml_node_ext_parent(njs_vm_t *vm, njs_object_prop_t *prop, static njs_int_t -njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, - njs_value_t *setval, njs_value_t *retval) +njs_xml_node_ext_remove_attribute(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) { - xmlNode *node, *current; - njs_int_t ret; - njs_value_t *push; + return njs_xml_node_ext_set_attribute(vm, args, nargs, 1); +} - current = njs_vm_external(vm, njs_xml_node_proto_id, value); - if (njs_slow_path(current == NULL || current->children == NULL)) { - njs_value_undefined_set(retval); - return NJS_DECLINED; - } - ret = njs_vm_array_alloc(vm, retval, 2); - if (njs_slow_path(ret != NJS_OK)) { +static njs_int_t +njs_xml_node_ext_remove_all_attributes(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused) +{ + xmlNode *current; + + current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0)); + if (njs_slow_path(current == NULL)) { + njs_vm_error(vm, "\"this\" is not a XMLNode object"); return NJS_ERROR; } - for (node = current->children; node != NULL; node = node->next) { - if (node->type != XML_ELEMENT_NODE) { - continue; - } - - push = njs_vm_array_push(vm, retval); - if (njs_slow_path(push == NULL)) { - return NJS_ERROR; - } - - ret = njs_vm_external_create(vm, push, njs_xml_node_proto_id, node, 0); - if (njs_slow_path(ret != NJS_OK)) { - return NJS_ERROR; - } + if (current->properties != NULL) { + xmlFreePropList(current->properties); + current->properties = NULL; } + njs_value_undefined_set(njs_vm_retval(vm)); + return NJS_OK; } static njs_int_t -njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, - njs_value_t *setval, njs_value_t *retval) +njs_xml_node_ext_remove_children(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) { - xmlNode *current, *node; - njs_int_t ret; - njs_chb_t chain; + xmlNode *current, *copy; + njs_str_t name; + njs_value_t *selector; - current = njs_vm_external(vm, njs_xml_node_proto_id, value); + current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0)); if (njs_slow_path(current == NULL)) { - njs_value_undefined_set(retval); - return NJS_DECLINED; + njs_vm_error(vm, "\"this\" is not a XMLNode object"); + return NJS_ERROR; } - njs_chb_init(&chain, njs_vm_memory_pool(vm)); + selector = njs_arg(args, nargs, 1); - for (node = current->children; node != NULL; node = node->next) { - if (node->type != XML_TEXT_NODE) { - continue; + njs_value_undefined_set(njs_vm_retval(vm)); + + if (!njs_value_is_null_or_undefined(selector)) { + if (njs_slow_path(!njs_value_is_string(selector))) { + njs_vm_error(vm, "selector is not a string"); + return NJS_ERROR; } - njs_chb_append(&chain, node->content, njs_strlen(node->content)); + njs_value_string_get(selector, &name); + + return njs_xml_node_tag_remove(vm, current, &name); } - ret = njs_vm_value_string_create_chb(vm, retval, &chain); + /* all. */ - njs_chb_destroy(&chain); + copy = xmlDocCopyNode(current, current->doc, 1); + if (njs_slow_path(copy == NULL)) { + njs_vm_error(vm, "xmlDocCopyNode() failed"); + return NJS_ERROR; + } - return ret; + if (copy->children != NULL) { + xmlFreeNodeList(copy->children); + copy->children = NULL; + } + + return njs_xml_replace_node(vm, current, copy); } -static int -njs_xml_buf_write_cb(void *context, const char *buffer, int len) +static njs_int_t +njs_xml_node_ext_remove_text(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) { - njs_chb_t *chain = context; - - njs_chb_append(chain, buffer, len); - - return chain->error ? -1 : len; + return njs_xml_node_ext_text(vm, NULL, njs_argument(args, 0), NULL, NULL); } -static int -njs_xml_node_one_contains(njs_xml_nset_t *nset, xmlNode *node, xmlNode *parent) +static njs_int_t +njs_xml_node_ext_set_attribute(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t remove) { - int in; - xmlNs ns; + xmlNode *current; + njs_str_t str; + njs_value_t *name; - if (nset->type == XML_NSET_TREE_NO_COMMENTS - && node->type == XML_COMMENT_NODE) - { - return 0; + current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0)); + if (njs_slow_path(current == NULL)) { + njs_vm_error(vm, "\"this\" is not a XMLNode object"); + return NJS_ERROR; } - in = 1; + name = njs_arg(args, nargs, 1); - if (nset->nodes != NULL) { - if (node->type != XML_NAMESPACE_DECL) { - in = xmlXPathNodeSetContains(nset->nodes, node); + if (njs_slow_path(!njs_value_is_string(name))) { + njs_vm_error(vm, "name is not a string"); + return NJS_ERROR; + } - } else { + njs_value_string_get(name, &str); - memcpy(&ns, node, sizeof(ns)); + return njs_xml_node_attr_handler(vm, current, &str, njs_arg(args, nargs, 2), + !remove ? njs_vm_retval(vm) : NULL); +} - /* libxml2 workaround, check xpath.c for details */ - if ((parent != NULL) && (parent->type == XML_ATTRIBUTE_NODE)) { - ns.next = (xmlNs *) parent->parent; +static njs_int_t +njs_xml_node_ext_set_text(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + return njs_xml_node_ext_text(vm, NULL, njs_argument(args, 0), + njs_arg(args, nargs, 1), njs_vm_retval(vm)); +} - } else { - ns.next = (xmlNs *) parent; - } - in = xmlXPathNodeSetContains(nset->nodes, (xmlNode *) &ns); - } - } +static njs_int_t +njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, + njs_value_t *setval, njs_value_t *retval) +{ + xmlNode *current; + njs_str_t name; + + current = njs_vm_external(vm, njs_xml_node_proto_id, value); + if (njs_slow_path(current == NULL || current->children == NULL)) { + njs_value_undefined_set(retval); + return NJS_DECLINED; + } + + name.start = NULL; + name.length = 0; + + return njs_xml_node_tags_handler(vm, current, &name, setval, retval); +} + + +static njs_int_t +njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *unused, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) +{ + u_char *text; + xmlNode *current, *copy; + njs_int_t ret; + njs_str_t content, enc; + + current = njs_vm_external(vm, njs_xml_node_proto_id, value); + if (njs_slow_path(current == NULL)) { + njs_value_undefined_set(retval); + return NJS_DECLINED; + } + + if (retval != NULL && setval == NULL) { + text = xmlNodeGetContent(current); + ret = njs_vm_value_string_create(vm, retval, text, njs_strlen(text)); + + xmlFree(text); + + return ret; + } + + /* set or delete. */ + + enc.start = NULL; + enc.length = 0; + + if (retval != NULL + && (setval != NULL && !njs_value_is_null_or_undefined(setval))) + { + if (njs_slow_path(!njs_value_is_string(setval))) { + njs_vm_error(vm, "setval is not a string"); + return NJS_ERROR; + } + + njs_value_string_get(setval, &content); + + ret = njs_xml_encode_special_chars(vm, &content, &enc); + if (njs_slow_path(ret != NJS_OK)) { + return NJS_ERROR; + } + } + + copy = xmlDocCopyNode(current, current->doc, 1); + if (njs_slow_path(copy == NULL)) { + njs_vm_error(vm, "xmlDocCopyNode() failed"); + return NJS_ERROR; + } + + xmlNodeSetContentLen(copy, enc.start, enc.length); + + if (retval != NULL) { + njs_value_undefined_set(retval); + } + + return njs_xml_replace_node(vm, current, copy); +} + + +static njs_int_t +njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name, + njs_value_t *setval, njs_value_t *retval) +{ + size_t size; + njs_int_t ret; + xmlAttr *attr; + const u_char *content, *value; + u_char name_buf[512], value_buf[1024]; + + if (retval != NULL && setval == NULL) { + /* get. */ + + for (attr = current->properties; attr != NULL; attr = attr->next) { + if (attr->type != XML_ATTRIBUTE_NODE) { + continue; + } + + size = njs_strlen(attr->name); + + if (name->length != size + || njs_strncmp(name->start, attr->name, size) != 0) + { + continue; + } + + if (attr->children != NULL + && attr->children->next == NULL + && attr->children->type == XML_TEXT_NODE) + { + content = (const u_char *) attr->children->content; + + return njs_vm_value_string_create(vm, retval, content, + njs_strlen(content)); + } + } + + njs_value_undefined_set(retval); + + return NJS_DECLINED; + } + + /* set or delete. */ + + ret = njs_xml_str_to_c_string(vm, name, &name_buf[0], sizeof(name_buf)); + if (njs_slow_path(ret != NJS_OK)) { + return NJS_ERROR; + } + + ret = xmlValidateQName(&name_buf[0], 0); + if (njs_slow_path(ret != 0)) { + njs_vm_error(vm, "attribute name \"%V\" is not valid", name); + return NJS_ERROR; + } + + if (retval == NULL + || (setval != NULL && njs_value_is_null_or_undefined(setval))) + { + /* delete. */ + + attr = xmlHasProp(current, &name_buf[0]); + + if (attr != NULL) { + xmlRemoveProp(attr); + } + + return NJS_OK; + } + + value = njs_xml_value_to_c_string(vm, setval, &value_buf[0], + sizeof(value_buf)); + if (njs_slow_path(value == NULL)) { + return NJS_ERROR; + } + + attr = xmlSetProp(current, &name_buf[0], value); + if (njs_slow_path(attr == NULL)) { + njs_vm_error(vm, "xmlSetProp() failed"); + return NJS_ERROR; + } + + njs_value_undefined_set(retval); + + return NJS_OK; +} + + +static njs_int_t +njs_xml_node_tag_remove(njs_vm_t *vm, xmlNode *current, njs_str_t *name) +{ + size_t size; + xmlNode *node, *next, *copy; + njs_int_t ret; + + copy = xmlDocCopyNode(current, current->doc, 1); + if (njs_slow_path(copy == NULL)) { + njs_vm_error(vm, "xmlDocCopyNode() failed"); + return NJS_ERROR; + } + + for (node = copy->children; node != NULL; node = next) { + next = node->next; + + if (node->type != XML_ELEMENT_NODE) { + continue; + } + + size = njs_strlen(node->name); + + if (name->length != size + || njs_strncmp(name->start, node->name, size) != 0) + { + continue; + } + + ret = njs_xml_replace_node(vm, node, NULL); + if (njs_slow_path(ret != NJS_OK)) { + xmlFreeNode(copy); + return NJS_ERROR; + } + } + + return njs_xml_replace_node(vm, current, copy); +} + + +static njs_int_t +njs_xml_node_tag_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name, + njs_value_t *setval, njs_value_t *retval) +{ + size_t size; + xmlNode *node; + + if (retval != NULL && setval == NULL) { + + /* get. */ + + for (node = current->children; node != NULL; node = node->next) { + if (node->type != XML_ELEMENT_NODE) { + continue; + } + + size = njs_strlen(node->name); + + if (name->length != size + || njs_strncmp(name->start, node->name, size) != 0) + { + continue; + } + + return njs_vm_external_create(vm, retval, njs_xml_node_proto_id, + node, 0); + } + + njs_value_undefined_set(retval); + + return NJS_DECLINED; + } + + if (retval != NULL) { + njs_vm_error(vm, "XMLNode.$tag$xxx is not assignable, use addChild() or" + "node.$tags = [node1, node2, ..] syntax"); + return NJS_ERROR; + } + + /* delete. */ + + return njs_xml_node_tag_remove(vm, current, name); +} + + +static njs_int_t +njs_xml_node_tags_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name, + njs_value_t *setval, njs_value_t *retval) +{ + size_t size; + int64_t i, length; + xmlNode *node, *copy; + njs_int_t ret; + njs_value_t *push; + njs_opaque_value_t *start; + + if (retval != NULL && setval == NULL) { + + /* get. */ + + ret = njs_vm_array_alloc(vm, retval, 2); + if (njs_slow_path(ret != NJS_OK)) { + return NJS_ERROR; + } + + for (node = current->children; node != NULL; node = node->next) { + if (node->type != XML_ELEMENT_NODE) { + continue; + } + + size = njs_strlen(node->name); + + if (name->length > 0 + && (name->length != size + || njs_strncmp(name->start, node->name, size) != 0)) + { + continue; + } + + push = njs_vm_array_push(vm, retval); + if (njs_slow_path(push == NULL)) { + return NJS_ERROR; + } + + ret = njs_vm_external_create(vm, push, njs_xml_node_proto_id, + node, 0); + if (njs_slow_path(ret != NJS_OK)) { + return NJS_ERROR; + } + } + + return NJS_OK; + } + + if (name->length > 0) { + njs_vm_error(vm, "XMLNode $tags$xxx is not assignable, use addChild() " + "or node.$tags = [node1, node2, ..] syntax"); + return NJS_ERROR; + } + + /* set or delete. */ + + copy = xmlDocCopyNode(current, current->doc, 1); + if (njs_slow_path(copy == NULL)) { + njs_vm_error(vm, "xmlDocCopyNode() failed"); + return NJS_ERROR; + } + + if (copy->children != NULL) { + xmlFreeNodeList(copy->children); + copy->children = NULL; + } + + if (retval == NULL) { + /* delete. */ + return njs_xml_replace_node(vm, current, copy); + } + + if (!njs_value_is_array(setval)) { + njs_vm_error(vm, "setval is not an array"); + goto error; + } + + start = (njs_opaque_value_t *) njs_vm_array_start(vm, setval); + if (njs_slow_path(start == NULL)) { + goto error; + } + + (void) njs_vm_array_length(vm, setval, &length); + + for (i = 0; i < length; i++) { + node = njs_xml_external_node(vm, njs_value_arg(start++)); + if (njs_slow_path(node == NULL)) { + njs_vm_error(vm, "setval[%D] is not a XMLNode object", i); + goto error; + } + + node = xmlDocCopyNode(node, current->doc, 1); + if (njs_slow_path(node == NULL)) { + njs_vm_error(vm, "xmlDocCopyNode() failed"); + goto error; + } + + node = xmlAddChild(copy, node); + if (njs_slow_path(node == NULL)) { + njs_vm_error(vm, "xmlAddChild() failed"); + xmlFreeNode(node); + goto error; + } + + ret = xmlReconciliateNs(current->doc, copy); + if (njs_slow_path(ret == -1)) { + njs_vm_error(vm, "xmlReconciliateNs() failed"); + return NJS_ERROR; + } + } + + njs_value_undefined_set(retval); + + return njs_xml_replace_node(vm, current, copy); + +error: + + xmlFreeNode(copy); + + return NJS_ERROR; +} + + +static xmlNode * +njs_xml_external_node(njs_vm_t *vm, njs_value_t *value) +{ + xmlNode *current; + njs_xml_doc_t *tree; + + current = njs_vm_external(vm, njs_xml_node_proto_id, value); + if (njs_slow_path(current == NULL)) { + tree = njs_vm_external(vm, njs_xml_doc_proto_id, value); + if (njs_slow_path(tree == NULL)) { + njs_vm_error(vm, "\"this\" is not a XMLNode object"); + return NULL; + } + + current = xmlDocGetRootElement(tree->doc); + if (njs_slow_path(current == NULL)) { + njs_vm_error(vm, "\"this\" is not a XMLNode object"); + return NULL; + } + } + + return current; +} + + +static njs_int_t +njs_xml_str_to_c_string(njs_vm_t *vm, njs_str_t *str, u_char *dst, + size_t size) +{ + u_char *p; + + if (njs_slow_path(str->length > size - njs_length("\0"))) { + njs_vm_error(vm, "njs_xml_str_to_c_string() very long string, " + "length >= %uz", size - njs_length("\0")); + return NJS_ERROR; + } + + p = njs_cpymem(dst, str->start, str->length); + *p = '\0'; + + return NJS_OK; +} + + +static const u_char * +njs_xml_value_to_c_string(njs_vm_t *vm, njs_value_t *value, u_char *dst, + size_t size) +{ + u_char *p; + njs_str_t str; + njs_int_t ret; + + ret = njs_vm_value_to_bytes(vm, &str, value); + if (njs_slow_path(ret != NJS_OK)) { + return NULL; + } + + if (njs_fast_path(str.length + njs_length("\0") < size)) { + ret = njs_xml_str_to_c_string(vm, &str, dst, size); + if (njs_slow_path(ret != NJS_OK)) { + return NULL; + } + + return dst; + } + + dst = njs_mp_alloc(njs_vm_memory_pool(vm), str.length + njs_length("\0")); + if (njs_slow_path(dst == NULL)) { + njs_vm_memory_error(vm); + return NULL; + } + + p = njs_cpymem(dst, str.start, str.length); + *p = '\0'; + + return dst; +} + + +static njs_int_t +njs_xml_encode_special_chars(njs_vm_t *vm, njs_str_t *src, njs_str_t *out) +{ + u_char *p, *dst, *end; + size_t len; + + len = 0; + end = src->start + src->length; + + for (p = src->start; p < end; p++) { + if (*p == '<' || *p == '>') { + len += njs_length("<"); + } + + if (*p == '&' || *p == '\r') { + len += njs_length("&"); + } + + if (*p == '"') { + len += njs_length("""); + } + + len += 1; + } + + if (njs_fast_path(len == src->length)) { + *out = *src; + return NJS_OK; + } + + out->start = njs_mp_alloc(njs_vm_memory_pool(vm), len); + if (njs_slow_path(out->start == NULL)) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + dst = out->start; + + for (p = src->start; p < end; p++) { + if (*p == '<') { + *dst++ = '&'; + *dst++ = 'l'; + *dst++ = 't'; + *dst++ = ';'; + + } else if (*p == '>') { + *dst++ = '&'; + *dst++ = 'g'; + *dst++ = 't'; + *dst++ = ';'; + + } else if (*p == '&') { + *dst++ = '&'; + *dst++ = 'a'; + *dst++ = 'm'; + *dst++ = 'p'; + *dst++ = ';'; + + } else if (*p == '"') { + *dst++ = '&'; + *dst++ = 'q'; + *dst++ = 'u'; + *dst++ = 'o'; + *dst++ = 't'; + *dst++ = ';'; + + } else if (*p == '\r') { + *dst++ = '&'; + *dst++ = '#'; + *dst++ = '1'; + *dst++ = '3'; + *dst++ = ';'; + + } else { + *dst++ = *p; + } + } + + out->length = len; + + return NJS_OK; +} + + +static njs_int_t +njs_xml_replace_node(njs_vm_t *vm, xmlNode *old, xmlNode *current) +{ + njs_mp_cleanup_t *cln; + + if (current != NULL) { + old = xmlReplaceNode(old, current); + + } else { + xmlUnlinkNode(old); + } + + cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0); + if (njs_slow_path(cln == NULL)) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + cln->handler = njs_xml_node_cleanup; + cln->data = old; + + return NJS_OK; +} + + +static void +njs_xml_node_cleanup(void *data) +{ + xmlNode *current = data; + + xmlFreeNode(current); +} + + +static void +njs_xml_doc_cleanup(void *data) +{ + njs_xml_doc_t *current = data; + + xmlFreeDoc(current->doc); + xmlFreeParserCtxt(current->ctx); +} + + +static int +njs_xml_buf_write_cb(void *context, const char *buffer, int len) +{ + njs_chb_t *chain = context; + + njs_chb_append(chain, buffer, len); + + return chain->error ? -1 : len; +} + + +static int +njs_xml_node_one_contains(njs_xml_nset_t *nset, xmlNode *node, xmlNode *parent) +{ + int in; + xmlNs ns; + + if (nset->type == XML_NSET_TREE_NO_COMMENTS + && node->type == XML_COMMENT_NODE) + { + return 0; + } + + in = 1; + + if (nset->nodes != NULL) { + if (node->type != XML_NAMESPACE_DECL) { + in = xmlXPathNodeSetContains(nset->nodes, node); + + } else { + + memcpy(&ns, node, sizeof(ns)); + + /* libxml2 workaround, check xpath.c for details */ + + if ((parent != NULL) && (parent->type == XML_ATTRIBUTE_NODE)) { + ns.next = (xmlNs *) parent->parent; + + } else { + ns.next = (xmlNs *) parent; + } + + in = xmlXPathNodeSetContains(nset->nodes, (xmlNode *) &ns); + } + } switch (nset->type) { case XML_NSET_TREE: @@ -961,7 +1705,7 @@ njs_xml_parse_ns_list(njs_vm_t *vm, njs_str_t *src) static njs_int_t njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, - njs_index_t exclusive) + njs_index_t magic) { u_char **prefix_list; ssize_t size; @@ -971,23 +1715,12 @@ njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_chb_t chain; njs_bool_t comments; njs_value_t *excluding, *prefixes; - njs_xml_doc_t *tree; njs_xml_nset_t *nset, *children; xmlOutputBuffer *buf; - current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 1)); + current = njs_xml_external_node(vm, njs_argument(args, 1)); if (njs_slow_path(current == NULL)) { - tree = njs_vm_external(vm, njs_xml_doc_proto_id, njs_argument(args, 1)); - if (njs_slow_path(tree == NULL)) { - njs_vm_error(vm, "\"this\" is not a XMLNode object"); - return NJS_ERROR; - } - - current = xmlDocGetRootElement(tree->doc); - if (njs_slow_path(current == NULL)) { - njs_vm_error(vm, "\"this\" is not a XMLNode object"); - return NJS_ERROR; - } + return NJS_ERROR; } comments = njs_value_bool(njs_arg(args, nargs, 3)); @@ -1053,7 +1786,7 @@ njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } ret = xmlC14NExecute(current->doc, njs_xml_c14n_visibility_cb, nset, - exclusive ? XML_C14N_EXCLUSIVE_1_0 : XML_C14N_1_0, + magic & 0x1 ? XML_C14N_EXCLUSIVE_1_0 : XML_C14N_1_0, prefix_list, comments, buf); if (njs_slow_path(ret < 0)) { @@ -1062,21 +1795,26 @@ njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto error; } - size = njs_chb_size(&chain); - if (njs_slow_path(size < 0)) { - njs_vm_memory_error(vm); - ret = NJS_ERROR; - goto error; - } + if (magic & 0x2) { + ret = njs_vm_value_string_create_chb(vm, njs_vm_retval(vm), &chain); - ret = njs_chb_join(&chain, &data); - if (njs_slow_path(ret != NJS_OK)) { - ret = NJS_ERROR; - goto error; - } + } else { + size = njs_chb_size(&chain); + if (njs_slow_path(size < 0)) { + njs_vm_memory_error(vm); + ret = NJS_ERROR; + goto error; + } + + ret = njs_chb_join(&chain, &data); + if (njs_slow_path(ret != NJS_OK)) { + ret = NJS_ERROR; + goto error; + } - ret = njs_vm_value_buffer_set(vm, njs_vm_retval(vm), data.start, - data.length); + ret = njs_vm_value_buffer_set(vm, njs_vm_retval(vm), data.start, + data.length); + } error: diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index a7651390..88e86f73 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -21819,10 +21819,14 @@ static njs_unit_test_t njs_webcrypto_test[] = }; +#define NJS_XML_DOC "const xml = require('xml');" \ + "let data = `ToveJani`;" \ + "let doc = xml.parse(data);" + + static njs_unit_test_t njs_xml_test[] = { - { njs_str("const xml = require('xml');" - "let doc = xml.parse(`ToveJani`);" + { njs_str(NJS_XML_DOC "[doc.note.$name," " doc.note.to.$text," " doc.note.$parent," @@ -21871,20 +21875,26 @@ static njs_unit_test_t njs_xml_test[] = "foo," "foo") }, - { njs_str("const xml = require('xml');" - "let doc = xml.parse(`ToveJani`);" + { njs_str(NJS_XML_DOC "let dec = new TextDecoder();" "dec.decode(xml.exclusiveC14n(doc.note))"), njs_str("ToveJani") }, - { njs_str("const xml = require('xml');" - "let doc = xml.parse(`ToveJani`);" + { njs_str(NJS_XML_DOC + "let dec = new TextDecoder();" + "dec.decode(xml.serialize(doc.note))"), + njs_str("ToveJani") }, + + { njs_str(NJS_XML_DOC + "xml.serializeToString(doc.note)"), + njs_str("ToveJani") }, + + { njs_str(NJS_XML_DOC "let dec = new TextDecoder();" "dec.decode(xml.exclusiveC14n(doc.note, doc.note.to))"), njs_str("Jani") }, - { njs_str("const xml = require('xml');" - "let doc = xml.parse(`ToveJani`);" + { njs_str(NJS_XML_DOC "njs.dump(doc)"), njs_str("XMLDoc {note:XMLNode {$name:'note'," "$tags:[XMLNode {$name:'to'," @@ -21892,8 +21902,7 @@ static njs_unit_test_t njs_xml_test[] = "$text:'Tove'}," "XMLNode {$name:'from',$text:'Jani'}]}}") }, - { njs_str("const xml = require('xml');" - "let doc = xml.parse(`ToveJani`);" + { njs_str(NJS_XML_DOC "JSON.stringify(doc)"), njs_str("{\"note\":{\"$name\":\"note\",\"$tags\":" "[{\"$name\":\"to\",\"$attrs\":{\"b\":\"bar\",\"a\":\"foo\"}," @@ -21902,6 +21911,180 @@ static njs_unit_test_t njs_xml_test[] = { njs_str("var xml = require('xml');" "var doc = xml.parse(``); xml.exclusiveC14n(doc, 1)"), njs_str("Error: \"excluding\" argument is not a XMLNode object") }, + + { njs_str(NJS_XML_DOC + "doc.$root.$text"), + njs_str("ToveJani") }, + + { njs_str(NJS_XML_DOC + "doc.$root.$text = 'WAKA';" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str("WAKA,WAKA") }, + + { njs_str(NJS_XML_DOC + "doc.$root.setText('WAKA');" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str("WAKA,WAKA") }, + + { njs_str(NJS_XML_DOC + "doc.$root.$text = '';" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str(",<WA&KA>") }, + + { njs_str(NJS_XML_DOC + "doc.$root.setText('');" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str(",<WA&KA>") }, + + { njs_str(NJS_XML_DOC + "doc.$root.$text = '\"WAKA\"';" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str("\"WAKA\",\"WAKA\"") }, + + { njs_str(NJS_XML_DOC + "doc.$root.$text = '';" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str(",") }, + + { njs_str(NJS_XML_DOC + "doc.$root.setText();" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str(",") }, + + { njs_str(NJS_XML_DOC + "doc.$root.setText(null);" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str(",") }, + + { njs_str(NJS_XML_DOC + "doc.$root.removeText();" + "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str(",") }, + + { njs_str(NJS_XML_DOC + "let to = doc.note.to;" + "doc.$root.$text = '';" + "[to.$name, to.$text, to.$attr$b, to.$parent.$name]"), + njs_str("to,Tove,bar,note") }, + + { njs_str(NJS_XML_DOC + "doc.$root.$text = 'WAKA';" + "doc.$root.$attr$aaa = 'foo';" + "doc.$root.$attr$bbb = 'bar';" + "[doc.$root.$attr$aaa, (new TextDecoder).decode(xml.c14n(doc))]"), + njs_str("foo,WAKA") }, + + { njs_str(NJS_XML_DOC + "doc.$root.$text = 'WAKA';" + "doc.$root.setAttribute('aaa', 'foo');" + "doc.$root.setAttribute('bbb', 'WAKA") }, + + { njs_str(NJS_XML_DOC + "doc.note.to.setAttribute('a', null);" + "(new TextDecoder).decode(xml.c14n(doc.note.to))"), + njs_str("Tove") }, + + { njs_str(NJS_XML_DOC + "doc.$root.setAttribute('<', 'xxx')"), + njs_str("Error: attribute name \"<\" is not valid") }, + + { njs_str(NJS_XML_DOC + "doc.$root.$text = 'WAKA';" + "doc.$root['$attr$' + 'x'.repeat(1024)] = 1;"), + njs_str("Error: njs_xml_str_to_c_string() very long string, length >= 511") }, + + { njs_str(NJS_XML_DOC + "delete doc.note.to.$attr$a;" + "(new TextDecoder).decode(xml.c14n(doc.note.to))"), + njs_str("Tove") }, + + { njs_str(NJS_XML_DOC + "doc.note.to.removeAttribute('a');" + "(new TextDecoder).decode(xml.c14n(doc.note.to))"), + njs_str("Tove") }, + + { njs_str(NJS_XML_DOC + "delete doc.note.to.removeAttribute('c');" + "(new TextDecoder).decode(xml.c14n(doc.note.to))"), + njs_str("Tove") }, + + { njs_str(NJS_XML_DOC + "delete doc.note.to.removeAllAttributes();" + "(new TextDecoder).decode(xml.c14n(doc.note.to))"), + njs_str("Tove") }, + + { njs_str(NJS_XML_DOC + "delete doc.note.$tag$to;" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("Jani") }, + + { njs_str(NJS_XML_DOC + "delete doc.note.to;" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("Jani") }, + + { njs_str("var xml = require('xml');" + "var doc = xml.parse(``);" + "delete doc.$root.a;" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("") }, + + { njs_str("var xml = require('xml');" + "var doc = xml.parse(``);" + "doc.$root.removeChildren('c');" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("") }, + + { njs_str("var xml = require('xml');" + "var doc = xml.parse(``);" + "doc.$root.removeChildren('a');" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("") }, + + { njs_str("var xml = require('xml');" + "var doc = xml.parse(``);" + "doc.$root.removeChildren();" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("") }, + + { njs_str(NJS_XML_DOC + "doc.note.$tags = [];" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("") }, + + { njs_str(NJS_XML_DOC + "var doc2 = xml.parse(``);" + "doc.note.addChild(doc2);" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("ToveJani") }, + + { njs_str(NJS_XML_DOC + "var doc2 = xml.parse(``);" + "doc.note.addChild(doc2);" + "doc.note.addChild(doc2);" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("ToveJani" + "") }, + + { njs_str(NJS_XML_DOC + "delete doc.note.$tags$;" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("") }, + + { njs_str(NJS_XML_DOC + "var doc2 = xml.parse(``);" + "doc.note.$tags = [doc.note.to, doc2];" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("Tove") }, + + { njs_str(NJS_XML_DOC + "var doc2 = xml.parse(``);" + "doc.note.$tags = [doc2, doc.note.to];" + "(new TextDecoder).decode(xml.c14n(doc))"), + njs_str("Tove") }, }; diff --git a/test/xml/saml_verify.t.js b/test/xml/saml_verify.t.js index 9a2d98a5..ec15c513 100644 --- a/test/xml/saml_verify.t.js +++ b/test/xml/saml_verify.t.js @@ -6,6 +6,14 @@ flags: [async] async function verify(params) { let file_data = fs.readFileSync(`test/xml/${params.saml}`); let key_data = fs.readFileSync(`test/webcrypto/${params.key.file}`); + + if (params.sign) { + let sign_key_data = fs.readFileSync(`test/webcrypto/${params.key.sign_file}`); + let signed = await signSAML(xml.parse(file_data), sign_key_data); + file_data = xml.c14n(signed); + //console.log((new TextDecoder()).decode(file_data)); + } + let saml = xml.parse(file_data); let r = await verifySAMLSignature(saml, key_data) @@ -27,6 +35,62 @@ async function verify(params) { return 'SUCCESS'; } +/* + * signSAML() signs a SAML message template + * + * The message to sign should already contain a full Signature element + * with SignedInfo and Reference elements below. + * The only parts that are missing are DigestValue and SignatureValue. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * The following signature algorithms are supported: + * - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 + * - http://www.w3.org/2000/09/xmldsig#rsa-sha1 + * + * The following digest algorithms are supported: + * - http://www.w3.org/2000/09/xmldsig#sha1 + * - http://www.w3.org/2001/04/xmlenc#sha256 + * + * As a part of the signing process, the following attributes are set: + * - xml:ID + * The value is a random 16 bytes hex string. + * - IssueInstant + * The value is the current time in ISO 8601 format. + * + * @param doc an XMLDoc object returned by xml.parse(). + * @param key_data is PKCS #8 in PEM format. + */ +async function signSAML(saml, key_data) { + const root = saml.$root; + const rootSignature = root.Signature; + + const rnd = Buffer.alloc(16); + crypto.getRandomValues(rnd); + const id = rnd.toString('hex'); + + root.setAttribute('xml:ID', id); + rootSignature.SignedInfo.Reference.setAttribute('URI', `#${id}`); + root.setAttribute('IssueInstant', (new Date()).toISOString()); + + await digestSAML(rootSignature, true); + await signatureSAML(rootSignature, key_data, true); + return saml; +} + /* * verifySAMLSignature() implements a verify clause * from Profiles for the OASIS SAML V2.0 @@ -61,21 +125,21 @@ async function verifySAMLSignature(saml, key_data) { const assertionSignature = assertion ? assertion.Signature : null; if (assertionSignature) { - if (!await verifyDigest(assertionSignature)) { + if (!await digestSAML(assertionSignature)) { return false; } - if (!await verifySignature(assertionSignature, key_data)) { + if (!await signatureSAML(assertionSignature, key_data)) { return false; } } if (rootSignature) { - if (!await verifyDigest(rootSignature)) { + if (!await digestSAML(rootSignature)) { return false; } - if (!await verifySignature(rootSignature, key_data)) { + if (!await signatureSAML(rootSignature, key_data)) { return false; } } @@ -83,7 +147,7 @@ async function verifySAMLSignature(saml, key_data) { return true; } -async function verifyDigest(signature) { +async function digestSAML(signature, produce) { const parent = signature.$parent; const signedInfo = signature.SignedInfo; const reference = signedInfo.Reference; @@ -137,12 +201,17 @@ async function verifyDigest(signature) { throw Error(`unexpected digest Algorithm ${alg}`); } - const expectedDigest = signedInfo.Reference.DigestValue.$text; - const c14n = xml.exclusiveC14n(parent, signature, withComments, prefixList); const dgst = await crypto.subtle.digest(hash, c14n); const b64dgst = Buffer.from(dgst).toString('base64'); + if (produce) { + signedInfo.Reference.DigestValue.$text = b64dgst; + return b64dgst; + } + + const expectedDigest = signedInfo.Reference.DigestValue.$text; + return expectedDigest === b64dgst; } @@ -159,9 +228,7 @@ function base64decode(b64) { return Buffer.from(joined, 'base64'); } -async function verifySignature(signature, key_data) { - const der = keyPem2Der(key_data, "PUBLIC"); - +async function signatureSAML(signature, key_data, produce) { let method, hash; const signedInfo = signature.SignedInfo; const alg = signedInfo.SignatureMethod.$attr$Algorithm; @@ -179,15 +246,27 @@ async function verifySignature(signature, key_data) { throw Error(`unexpected signature Algorithm ${alg}`); } - const expectedValue = base64decode(signature.SignatureValue.$text); const withComments = signedInfo.CanonicalizationMethod .$attr$Algorithm.slice(39) == 'WithComments'; const signedInfoC14n = xml.exclusiveC14n(signedInfo, null, withComments); + if (produce) { + const der = keyPem2Der(key_data, "PRIVATE"); + const key = await crypto.subtle.importKey("pkcs8", der, { name: method, hash }, + false, [ "sign" ]); + + let sig = await crypto.subtle.sign({ name: method }, key, signedInfoC14n); + + signature.SignatureValue.$text = Buffer.from(sig).toString('base64'); + return signature; + } + + const der = keyPem2Der(key_data, "PUBLIC"); const key = await crypto.subtle.importKey("spki", der, { name: method, hash }, - false, [ "verify" ]); + false, [ "verify" ]); + const expectedValue = base64decode(signature.SignatureValue.$text); return await crypto.subtle.verify({ name: method }, key, expectedValue, signedInfoC14n); } @@ -220,6 +299,8 @@ let saml_verify_tsuite = { { saml: "response_signed_broken2.xml", expected: false }, { saml: "response_signed.xml", key: { file: "rsa2.spki"}, expected: false }, { saml: "response_assertion_and_message_signed.xml", expected: true }, + + { saml: "auth_r.xml", sign: true, key: { sign_file: "rsa.pkcs8" }, expected: true }, ]}; run([ diff --git a/ts/njs_modules/xml.d.ts b/ts/njs_modules/xml.d.ts index 61c707ac..39082ba3 100644 --- a/ts/njs_modules/xml.d.ts +++ b/ts/njs_modules/xml.d.ts @@ -72,8 +72,9 @@ declare module "xml" { export interface XMLNode { /** * node.$attr$xxx - the node's attribute value of "xxx". + * @since 0.7.11 the property is writable. */ - readonly [key: `$attr$${string}`]: string | undefined; + [key: `$attr$${string}`]: string | undefined; /** * node.$attrs - an XMLAttr wrapper object for all the attributes @@ -83,13 +84,15 @@ declare module "xml" { /** * node.$tag$xxx - the node's first child tag named "xxx". + * @since 0.7.11 the property is writable. */ - readonly [key: `$tag$${string}`]: XMLNode | undefined; + [key: `$tag$${string}`]: XMLNode | undefined; /** * node.$tags$xxx - all children tags named "xxx" of the node. + * @since 0.7.11 the property is writable. */ - readonly [key: `$tags$${string}`]: XMLNode[] | undefined; + [key: `$tags$${string}`]: XMLNode[] | undefined; /** * node.$name - the name of the node. @@ -108,8 +111,9 @@ declare module "xml" { /** * node.$text - the content of the node. + * @since 0.7.11 the property is writable. */ - readonly $text: string; + $text: string; /** * node.$tags - all the node's children tags. @@ -118,8 +122,9 @@ declare module "xml" { /** * node.xxx is the same as node.$tag$xxx. + * @since 0.7.11 the property is writable. */ - readonly [key: XMLTagName]: XMLNode | undefined; + [key: XMLTagName]: XMLNode | undefined; } export interface XMLAttr { @@ -130,6 +135,15 @@ declare module "xml" { } interface Xml { + /** + * Canonicalizes root_node and its children according to + * https://www.w3.org/TR/xml-c14n/. + * + * @param root - XMLDoc or XMLNode. + * @return Buffer object containing canonicalized output. + */ + c14n(root: XMLDoc | XMLNode): Buffer; + /** * Parses src buffer for an XML document and returns a wrapper object. * @@ -140,20 +154,32 @@ declare module "xml" { /** * Canonicalizes root_node and its children according to - * https://www.w3.org/TR/xml-exc-c14n/. + * https://www.w3.org/tr/xml-exc-c14n/. * * @param root - XMLDoc or XMLNode. * @param excluding_node - allows to omit from the output a part of the * document corresponding to the excluding_node and its children. - * @param withComments - a boolean (false by default). When withComments + * @param withComments - a boolean (false by default). when withComments * is true canonicalization corresponds to * http://www.w3.org/2001/10/xml-exc-c14n#WithComments. * @param prefix_list - an optional string with a space separated namespace * prefixes for namespaces that should also be included into the output. - * @return Buffer object containing canonicalized output. + * @return buffer object containing canonicalized output. */ exclusiveC14n(root: XMLDoc | XMLNode, excluding_node?: XMLNode | null | undefined, withComments?: boolean, prefix_list?: string): Buffer; + + /** + * The alias to xml.x14n() + * @since 0.7.11 + */ + serialize(root: XMLDoc | XMLNode): Buffer; + + /** + * The same as xml.x14n() but returns the retval as a string. + * @since 0.7.11 + */ + serializeToString(root: XMLDoc | XMLNode): string; } const xml: Xml; -- 2.47.3