|
1 | | -# Copyright © 2011-2024 Splunk, Inc. |
| 1 | +# Copyright © 2011-2026 Splunk, Inc. |
2 | 2 | # |
3 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may |
4 | 4 | # not use this file except in compliance with the License. You may obtain |
@@ -862,6 +862,158 @@ def post( |
862 | 862 | response = self.http.post(path, all_headers, **query) |
863 | 863 | return response |
864 | 864 |
|
| 865 | + @_authentication |
| 866 | + @_log_duration |
| 867 | + def put( |
| 868 | + self, |
| 869 | + path_segment, |
| 870 | + owner=None, |
| 871 | + app=None, |
| 872 | + sharing=None, |
| 873 | + headers=None, |
| 874 | + **query, |
| 875 | + ): |
| 876 | + """Performs a PUT operation from the REST path segment with the given object, |
| 877 | + namespace and query. |
| 878 | +
|
| 879 | + This method is named to match the HTTP method. ``put`` makes at least |
| 880 | + one round trip to the server, one additional round trip for each 303 |
| 881 | + status returned, and at most two additional round trips if |
| 882 | + the ``autologin`` field of :func:`connect` is set to ``True``. |
| 883 | +
|
| 884 | + If *owner*, *app*, and *sharing* are omitted, this method uses the |
| 885 | + default :class:`Context` namespace. All other keyword arguments are |
| 886 | + included in the URL as query parameters. |
| 887 | +
|
| 888 | + If you provide a ``body`` argument to ``put``, it will be used as the PUT body, |
| 889 | + and all other keyword arguments will be passed as GET-style arguments in the URL. |
| 890 | +
|
| 891 | + :raises AuthenticationError: Raised when the ``Context`` object is not |
| 892 | + logged in. |
| 893 | + :raises HTTPError: Raised when an error occurred in a PUT operation from |
| 894 | + *path_segment*. |
| 895 | + :param path_segment: A REST path segment. |
| 896 | + :type path_segment: ``string`` |
| 897 | + :param owner: The owner context of the namespace (optional). |
| 898 | + :type owner: ``string`` |
| 899 | + :param app: The app context of the namespace (optional). |
| 900 | + :type app: ``string`` |
| 901 | + :param sharing: The sharing mode of the namespace (optional). |
| 902 | + :type sharing: ``string`` |
| 903 | + :param headers: List of extra HTTP headers to send (optional). |
| 904 | + :type headers: ``list`` of 2-tuples. |
| 905 | + :param query: All other keyword arguments, which are used as query |
| 906 | + parameters. |
| 907 | + :param body: Parameters to be used in the put body. If specified, |
| 908 | + any parameters in the query will be applied to the URL instead of |
| 909 | + the body. If a dict is supplied, the key-value pairs will be form |
| 910 | + encoded. If a string is supplied, the body will be passed through |
| 911 | + in the request unchanged. |
| 912 | + :type body: ``dict`` or ``str`` |
| 913 | + :return: The response from the server. |
| 914 | + :rtype: ``dict`` with keys ``body``, ``headers``, ``reason``, |
| 915 | + and ``status`` |
| 916 | +
|
| 917 | + **Example**:: |
| 918 | +
|
| 919 | + c = binding.connect(...) |
| 920 | + # Call an HTTP endpoint, exposed as Custom Rest Endpoint in a Splunk App. |
| 921 | + # PUT /servicesNS/-/app_name/custom_rest_endpoint |
| 922 | + c.put( |
| 923 | + app="app_name", |
| 924 | + path_segment="custom_rest_endpoint", |
| 925 | + body=json.dumps({"key": "val"}), |
| 926 | + headers=[("Content-Type", "application/json")], |
| 927 | + ) |
| 928 | + """ |
| 929 | + if headers is None: |
| 930 | + headers = [] |
| 931 | + |
| 932 | + path = self.authority + self._abspath( |
| 933 | + path_segment, owner=owner, app=app, sharing=sharing |
| 934 | + ) |
| 935 | + |
| 936 | + logger.debug("PUT request to %s (body: %s)", path, mask_sensitive_data(query)) |
| 937 | + all_headers = headers + self.additional_headers + self._auth_headers |
| 938 | + response = self.http.put(path, all_headers, **query) |
| 939 | + return response |
| 940 | + |
| 941 | + @_authentication |
| 942 | + @_log_duration |
| 943 | + def patch( |
| 944 | + self, |
| 945 | + path_segment, |
| 946 | + owner=None, |
| 947 | + app=None, |
| 948 | + sharing=None, |
| 949 | + headers=None, |
| 950 | + **query, |
| 951 | + ): |
| 952 | + """Performs a PATCH operation from the REST path segment with the given object, |
| 953 | + namespace and query. |
| 954 | +
|
| 955 | + This method is named to match the HTTP method. ``patch`` makes at least |
| 956 | + one round trip to the server, one additional round trip for each 303 |
| 957 | + status returned, and at most two additional round trips if |
| 958 | + the ``autologin`` field of :func:`connect` is set to ``True``. |
| 959 | +
|
| 960 | + If *owner*, *app*, and *sharing* are omitted, this method uses the |
| 961 | + default :class:`Context` namespace. All other keyword arguments are |
| 962 | + included in the URL as query parameters. |
| 963 | +
|
| 964 | + If you provide a ``body`` argument to ``patch``, it will be used as the PATCH body, |
| 965 | + and all other keyword arguments will be passed as GET-style arguments in the URL. |
| 966 | +
|
| 967 | + :raises AuthenticationError: Raised when the ``Context`` object is not |
| 968 | + logged in. |
| 969 | + :raises HTTPError: Raised when an error occurred in a PATCH operation from |
| 970 | + *path_segment*. |
| 971 | + :param path_segment: A REST path segment. |
| 972 | + :type path_segment: ``string`` |
| 973 | + :param owner: The owner context of the namespace (optional). |
| 974 | + :type owner: ``string`` |
| 975 | + :param app: The app context of the namespace (optional). |
| 976 | + :type app: ``string`` |
| 977 | + :param sharing: The sharing mode of the namespace (optional). |
| 978 | + :type sharing: ``string`` |
| 979 | + :param headers: List of extra HTTP headers to send (optional). |
| 980 | + :type headers: ``list`` of 2-tuples. |
| 981 | + :param query: All other keyword arguments, which are used as query |
| 982 | + parameters. |
| 983 | + :param body: Parameters to be used in the patch body. If specified, |
| 984 | + any parameters in the query will be applied to the URL instead of |
| 985 | + the body. If a dict is supplied, the key-value pairs will be form |
| 986 | + encoded. If a string is supplied, the body will be passed through |
| 987 | + in the request unchanged. |
| 988 | + :type body: ``dict`` or ``str`` |
| 989 | + :return: The response from the server. |
| 990 | + :rtype: ``dict`` with keys ``body``, ``headers``, ``reason``, |
| 991 | + and ``status`` |
| 992 | +
|
| 993 | + **Example**:: |
| 994 | +
|
| 995 | + c = binding.connect(...) |
| 996 | + # Call an HTTP endpoint, exposed as Custom Rest Endpoint in a Splunk App. |
| 997 | + # PATCH /servicesNS/-/app_name/custom_rest_endpoint |
| 998 | + c.patch( |
| 999 | + app="app_name", |
| 1000 | + path_segment="custom_rest_endpoint", |
| 1001 | + body=json.dumps({"key": "val"}), |
| 1002 | + headers=[("Content-Type", "application/json")], |
| 1003 | + ) |
| 1004 | + """ |
| 1005 | + if headers is None: |
| 1006 | + headers = [] |
| 1007 | + |
| 1008 | + path = self.authority + self._abspath( |
| 1009 | + path_segment, owner=owner, app=app, sharing=sharing |
| 1010 | + ) |
| 1011 | + |
| 1012 | + logger.debug("PATCH request to %s (body: %s)", path, mask_sensitive_data(query)) |
| 1013 | + all_headers = headers + self.additional_headers + self._auth_headers |
| 1014 | + response = self.http.patch(path, all_headers, **query) |
| 1015 | + return response |
| 1016 | + |
865 | 1017 | @_authentication |
866 | 1018 | @_log_duration |
867 | 1019 | def request( |
@@ -1305,6 +1457,40 @@ def __init__( |
1305 | 1457 | self.retries = retries |
1306 | 1458 | self.retryDelay = retryDelay |
1307 | 1459 |
|
| 1460 | + def _prepare_request_body_and_url(self, url, headers, **kwargs): |
| 1461 | + """Helper function to prepare the request body and URL. |
| 1462 | +
|
| 1463 | + :param url: The URL. |
| 1464 | + :type url: ``string`` |
| 1465 | + :param headers: A list of pairs specifying the headers for the HTTP request. |
| 1466 | + :type headers: ``list`` |
| 1467 | + :param kwargs: Additional keyword arguments (optional). |
| 1468 | + :type kwargs: ``dict`` |
| 1469 | + :returns: A tuple containing the updated URL, headers, and body. |
| 1470 | + :rtype: ``tuple`` |
| 1471 | + """ |
| 1472 | + if headers is None: |
| 1473 | + headers = [] |
| 1474 | + |
| 1475 | + # We handle GET-style arguments and an unstructured body. This is here |
| 1476 | + # to support the receivers/stream endpoint. |
| 1477 | + if "body" in kwargs: |
| 1478 | + # We only use application/x-www-form-urlencoded if there is no other |
| 1479 | + # Content-Type header present. This can happen in cases where we |
| 1480 | + # send requests as application/json, e.g. for KV Store. |
| 1481 | + if len([x for x in headers if x[0].lower() == "content-type"]) == 0: |
| 1482 | + headers.append(("Content-Type", "application/x-www-form-urlencoded")) |
| 1483 | + |
| 1484 | + body = kwargs.pop("body") |
| 1485 | + if isinstance(body, dict): |
| 1486 | + body = _encode(**body).encode("utf-8") |
| 1487 | + if len(kwargs) > 0: |
| 1488 | + url = url + UrlEncoded("?" + _encode(**kwargs), skip_encode=True) |
| 1489 | + else: |
| 1490 | + body = _encode(**kwargs).encode("utf-8") |
| 1491 | + |
| 1492 | + return url, headers, body |
| 1493 | + |
1308 | 1494 | def delete(self, url, headers=None, **kwargs): |
1309 | 1495 | """Sends a DELETE request to a URL. |
1310 | 1496 |
|
@@ -1379,26 +1565,52 @@ def post(self, url, headers=None, **kwargs): |
1379 | 1565 | its structure). |
1380 | 1566 | :rtype: ``dict`` |
1381 | 1567 | """ |
1382 | | - if headers is None: |
1383 | | - headers = [] |
| 1568 | + url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs) |
| 1569 | + message = {"method": "POST", "headers": headers, "body": body} |
| 1570 | + return self.request(url, message) |
1384 | 1571 |
|
1385 | | - # We handle GET-style arguments and an unstructured body. This is here |
1386 | | - # to support the receivers/stream endpoint. |
1387 | | - if "body" in kwargs: |
1388 | | - # We only use application/x-www-form-urlencoded if there is no other |
1389 | | - # Content-Type header present. This can happen in cases where we |
1390 | | - # send requests as application/json, e.g. for KV Store. |
1391 | | - if len([x for x in headers if x[0].lower() == "content-type"]) == 0: |
1392 | | - headers.append(("Content-Type", "application/x-www-form-urlencoded")) |
| 1572 | + def put(self, url, headers=None, **kwargs): |
| 1573 | + """Sends a PUT request to a URL. |
1393 | 1574 |
|
1394 | | - body = kwargs.pop("body") |
1395 | | - if isinstance(body, dict): |
1396 | | - body = _encode(**body).encode("utf-8") |
1397 | | - if len(kwargs) > 0: |
1398 | | - url = url + UrlEncoded("?" + _encode(**kwargs), skip_encode=True) |
1399 | | - else: |
1400 | | - body = _encode(**kwargs).encode("utf-8") |
1401 | | - message = {"method": "POST", "headers": headers, "body": body} |
| 1575 | + :param url: The URL. |
| 1576 | + :type url: ``string`` |
| 1577 | + :param headers: A list of pairs specifying the headers for the HTTP |
| 1578 | + response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``). |
| 1579 | + :type headers: ``list`` |
| 1580 | + :param kwargs: Additional keyword arguments (optional). If the argument |
| 1581 | + is ``body``, the value is used as the body for the request, and the |
| 1582 | + keywords and their arguments will be URL encoded. If there is no |
| 1583 | + ``body`` keyword argument, all the keyword arguments are encoded |
| 1584 | + into the body of the request in the format ``x-www-form-urlencoded``. |
| 1585 | + :type kwargs: ``dict`` |
| 1586 | + :returns: A dictionary describing the response (see :class:`HttpLib` for |
| 1587 | + its structure). |
| 1588 | + :rtype: ``dict`` |
| 1589 | + """ |
| 1590 | + url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs) |
| 1591 | + message = {"method": "PUT", "headers": headers, "body": body} |
| 1592 | + return self.request(url, message) |
| 1593 | + |
| 1594 | + def patch(self, url, headers=None, **kwargs): |
| 1595 | + """Sends a PATCH request to a URL. |
| 1596 | +
|
| 1597 | + :param url: The URL. |
| 1598 | + :type url: ``string`` |
| 1599 | + :param headers: A list of pairs specifying the headers for the HTTP |
| 1600 | + response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``). |
| 1601 | + :type headers: ``list`` |
| 1602 | + :param kwargs: Additional keyword arguments (optional). If the argument |
| 1603 | + is ``body``, the value is used as the body for the request, and the |
| 1604 | + keywords and their arguments will be URL encoded. If there is no |
| 1605 | + ``body`` keyword argument, all the keyword arguments are encoded |
| 1606 | + into the body of the request in the format ``x-www-form-urlencoded``. |
| 1607 | + :type kwargs: ``dict`` |
| 1608 | + :returns: A dictionary describing the response (see :class:`HttpLib` for |
| 1609 | + its structure). |
| 1610 | + :rtype: ``dict`` |
| 1611 | + """ |
| 1612 | + url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs) |
| 1613 | + message = {"method": "PATCH", "headers": headers, "body": body} |
1402 | 1614 | return self.request(url, message) |
1403 | 1615 |
|
1404 | 1616 | def request(self, url, message, **kwargs): |
@@ -1550,7 +1762,21 @@ def connect(scheme, host, port): |
1550 | 1762 | kwargs["cert_file"] = cert_file |
1551 | 1763 |
|
1552 | 1764 | if not verify: |
1553 | | - kwargs["context"] = ssl._create_unverified_context() # nosemgrep |
| 1765 | + ctx = ssl._create_unverified_context() # nosemgrep |
| 1766 | + # Support all ML-KEM key exchange algorithms, by default OpenSSL only |
| 1767 | + # includes the X25519MLKEM768 from all of the below listed MLKEM key |
| 1768 | + # exchanges. |
| 1769 | + # |
| 1770 | + # set_groups method is only available with Python 3.15, but Splunk comes |
| 1771 | + # with patched python that includes set_groups on 3.9 and 3.13, thus we |
| 1772 | + # check for the existence of set_groups, not the python version. |
| 1773 | + if hasattr(ctx, "set_groups"): |
| 1774 | + ctx.set_groups( # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue] |
| 1775 | + "X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024:" |
| 1776 | + + "MLKEM512:MLKEM768:MLKEM1024:" |
| 1777 | + + "X25519:secp256r1:X448:secp384r1:secp521r1:ffdhe2048:ffdhe3072" |
| 1778 | + ) |
| 1779 | + kwargs["context"] = ctx |
1554 | 1780 | elif context: |
1555 | 1781 | # verify is True in elif branch and context is not None |
1556 | 1782 | kwargs["context"] = context |
|
0 commit comments