1
0

2 Коммиты 6be32a540d ... 5ead179b8e

Автор SHA1 Сообщение Дата
  yhirose 5ead179b8e Update README 3 дней назад
  Miko 1942e0ef01 Add C++ modules support (#2291) 2 дней назад
5 измененных файлов с 211 добавлено и 22 удалено
  1. 48 3
      CMakeLists.txt
  2. 55 8
      README.md
  3. 16 0
      cmake/modules.cmake
  4. 77 0
      generate_module.py
  5. 15 11
      split.py

+ 48 - 3
CMakeLists.txt

@@ -5,6 +5,7 @@
 	* HTTPLIB_USE_ZLIB_IF_AVAILABLE (default on)
 	* HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on)
 	* HTTPLIB_USE_ZSTD_IF_AVAILABLE (default on)
+	* HTTPLIB_BUILD_MODULES (default off)
 	* HTTPLIB_REQUIRE_OPENSSL (default off)
 	* HTTPLIB_REQUIRE_ZLIB (default off)
 	* HTTPLIB_REQUIRE_BROTLI (default off)
@@ -110,6 +111,15 @@ option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system cer
 option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON)
 option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF)
 option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
+# C++20 modules support requires CMake 3.28 or later
+if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28")
+	option(HTTPLIB_BUILD_MODULES "Build httplib modules (requires HTTPLIB_COMPILE to be ON)." OFF)
+else()
+	set(HTTPLIB_BUILD_MODULES OFF CACHE INTERNAL "Build httplib modules disabled (requires CMake 3.28+)" FORCE)
+	if(DEFINED CACHE{HTTPLIB_BUILD_MODULES} AND HTTPLIB_BUILD_MODULES)
+		message(WARNING "HTTPLIB_BUILD_MODULES requires CMake 3.28 or later. Current version is ${CMAKE_VERSION}. Modules support has been disabled.")
+	endif()
+endif()
 # Defaults to static library but respects standard BUILD_SHARED_LIBS if set
 include(CMakeDependentOption)
 cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only."
@@ -240,6 +250,22 @@ if(HTTPLIB_COMPILE)
 		message(FATAL_ERROR "Failed when trying to split cpp-httplib with the Python script.\n${_httplib_split_error}")
 	endif()
 
+	# If building modules, also generate the module file
+	if(HTTPLIB_BUILD_MODULES)
+		# Put the generate_module script into the build dir
+		configure_file(generate_module.py "${CMAKE_CURRENT_BINARY_DIR}/generate_module.py"
+			COPYONLY
+		)
+		# Generate the module file
+		execute_process(COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/generate_module.py"
+			WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+			ERROR_VARIABLE _httplib_module_error
+		)
+		if(_httplib_module_error)
+			message(FATAL_ERROR "Failed when trying to generate cpp-httplib module with the Python script.\n${_httplib_module_error}")
+		endif()
+	endif()
+
 	# split.py puts output in "out"
 	set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out")
 	add_library(${PROJECT_NAME} ${HTTPLIB_LIB_TYPE} "${_httplib_build_includedir}/httplib.cc")
@@ -248,6 +274,13 @@ if(HTTPLIB_COMPILE)
 			$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
 			$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h>
 	)
+	
+	# Add C++20 module support if requested
+	# Include from separate file to prevent parse errors on older CMake versions
+	if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28")
+		include(cmake/modules.cmake)
+	endif()
+	
 	set_target_properties(${PROJECT_NAME}
 		PROPERTIES
 			VERSION ${${PROJECT_NAME}_VERSION}
@@ -264,8 +297,12 @@ endif()
 # Only useful if building in-tree, versus using it from an installation.
 add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
 
-# Require C++11
-target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
+# Require C++11, or C++20 if modules are enabled
+if(HTTPLIB_BUILD_MODULES)
+	target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_20)
+else()
+	target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
+endif()
 
 target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC}
 	$<BUILD_INTERFACE:${_httplib_build_includedir}>
@@ -337,7 +374,11 @@ if(HTTPLIB_INSTALL)
 	# Creates the export httplibTargets.cmake
 	# This is strictly what holds compilation requirements
 	# and linkage information (doesn't find deps though).
-	install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets)
+	if(HTTPLIB_BUILD_MODULES)
+		install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/httplib/modules CXX_MODULES_BMI DESTINATION ${CMAKE_INSTALL_LIBDIR}/httplib/modules)
+	else()
+		install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets)
+	endif()
 
 	install(FILES "${_httplib_build_includedir}/httplib.h" TYPE INCLUDE)
 
@@ -366,6 +407,10 @@ if(HTTPLIB_INSTALL)
 	include(CPack)
 endif()
 
+if(HTTPLIB_BUILD_MODULES AND NOT HTTPLIB_COMPILE)
+	message(FATAL_ERROR "HTTPLIB_BUILD_MODULES requires HTTPLIB_COMPILE to be ON.")
+endif()
+
 if(HTTPLIB_TEST)
 	include(CTest)
 	add_subdirectory(test)

+ 55 - 8
README.md

@@ -97,10 +97,13 @@ cli.enable_server_hostname_verification(false);
 
 ### SSL Error Handling
 
-When SSL operations fail, cpp-httplib provides detailed error information through two separate error fields:
+When SSL operations fail, cpp-httplib provides detailed error information through `ssl_error()` and `ssl_backend_error()`:
+
+- `ssl_error()` - Returns the TLS-level error code (e.g., `SSL_ERROR_SSL` for OpenSSL)
+- `ssl_backend_error()` - Returns the backend-specific error code (e.g., `ERR_get_error()` for OpenSSL, return value for Mbed TLS)
 
 ```c++
-#define CPPHTTPLIB_OPENSSL_SUPPORT
+#define CPPHTTPLIB_OPENSSL_SUPPORT  // or CPPHTTPLIB_MBEDTLS_SUPPORT
 #include "path/to/httplib.h"
 
 httplib::Client cli("https://example.com");
@@ -117,18 +120,18 @@ if (!res) {
       break;
 
     case httplib::Error::SSLLoadingCerts:
-      std::cout << "SSL cert loading failed, OpenSSL error: "
-                << std::hex << res.ssl_openssl_error() << std::endl;
+      std::cout << "SSL cert loading failed, backend error: "
+                << std::hex << res.ssl_backend_error() << std::endl;
       break;
 
     case httplib::Error::SSLServerVerification:
-      std::cout << "SSL verification failed, X509 error: "
-                << res.ssl_openssl_error() << std::endl;
+      std::cout << "SSL verification failed, verify error: "
+                << res.ssl_backend_error() << std::endl;
       break;
 
     case httplib::Error::SSLServerHostnameVerification:
-      std::cout << "SSL hostname verification failed, X509 error: "
-                << res.ssl_openssl_error() << std::endl;
+      std::cout << "SSL hostname verification failed, verify error: "
+                << res.ssl_backend_error() << std::endl;
       break;
 
     default:
@@ -137,6 +140,50 @@ if (!res) {
 }
 ```
 
+### Custom Certificate Verification
+
+You can set a custom verification callback using `tls::VerifyCallback`:
+
+```c++
+httplib::Client cli("https://example.com");
+
+cli.set_server_certificate_verifier(
+    [](const httplib::tls::VerifyContext &ctx) -> bool {
+      std::cout << "Subject CN: " << ctx.subject_cn() << std::endl;
+      std::cout << "Issuer: " << ctx.issuer_name() << std::endl;
+      std::cout << "Depth: " << ctx.depth << std::endl;
+      std::cout << "Pre-verified: " << ctx.preverify_ok << std::endl;
+
+      // Inspect SANs (Subject Alternative Names)
+      for (const auto &san : ctx.sans()) {
+        std::cout << "SAN: " << san.value << std::endl;
+      }
+
+      // Return true to accept, false to reject
+      return ctx.preverify_ok;
+    });
+```
+
+### Peer Certificate Inspection
+
+On the server side, you can inspect the client's peer certificate from a request handler:
+
+```c++
+httplib::SSLServer svr("./cert.pem", "./key.pem",
+                       "./client-ca-cert.pem");
+
+svr.Get("/", [](const httplib::Request &req, httplib::Response &res) {
+  auto cert = req.peer_cert();
+  if (cert) {
+    std::cout << "Client CN: " << cert.subject_cn() << std::endl;
+    std::cout << "Serial: " << cert.serial() << std::endl;
+  }
+
+  auto sni = req.sni();
+  std::cout << "SNI: " << sni << std::endl;
+});
+```
+
 Server
 ------
 

+ 16 - 0
cmake/modules.cmake

@@ -0,0 +1,16 @@
+# This file contains C++20 module support requiring CMake 3.28+
+# Included conditionally to prevent parse errors on older CMake versions
+
+if(HTTPLIB_BUILD_MODULES)
+	if(POLICY CMP0155)
+		cmake_policy(SET CMP0155 NEW)
+	endif()
+
+	set(CMAKE_CXX_SCAN_FOR_MODULES ON)
+	
+	target_sources(${PROJECT_NAME}
+		PUBLIC
+			FILE_SET CXX_MODULES FILES
+				"${_httplib_build_includedir}/httplib.cppm"
+	)
+endif()

+ 77 - 0
generate_module.py

@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+
+"""This script generates httplib.cppm module file from httplib.h."""
+
+import os
+import sys
+from argparse import ArgumentParser, Namespace
+from typing import List
+
+def main() -> None:
+    """Main entry point for the script."""
+
+    args_parser: ArgumentParser = ArgumentParser(description=__doc__)
+    args_parser.add_argument(
+        "-o", "--out", help="where to write the files (default: out)", default="out"
+    )
+    args: Namespace = args_parser.parse_args()
+
+    cur_dir: str = os.path.dirname(sys.argv[0])
+    if not cur_dir:
+        cur_dir = '.'
+    lib_name: str = "httplib"
+    header_name: str = f"/{lib_name}.h"
+    # get the input file
+    in_file: str = f"{cur_dir}{header_name}"
+    # get the output file
+    cppm_out: str = f"{args.out}/{lib_name}.cppm"
+
+    # if the modification time of the out file is after the in file,
+    # don't generate (as it is already finished)
+    do_generate: bool = True
+
+    if os.path.exists(cppm_out):
+        in_time: float = os.path.getmtime(in_file)
+        out_time: float = os.path.getmtime(cppm_out)
+        do_generate: bool = in_time > out_time
+
+    if do_generate:
+        with open(in_file) as f:
+            lines: List[str] = f.readlines()
+
+        os.makedirs(args.out, exist_ok=True)
+
+        # Find the Headers and Declaration comment markers
+        headers_start: int = -1
+        declaration_start: int = -1
+        for i, line in enumerate(lines):
+            if ' * Headers' in line:
+                headers_start = i - 1  # Include the /* line
+            elif ' * Declaration' in line:
+                declaration_start = i - 1  # Stop before the /* line
+                break
+
+        with open(cppm_out, 'w') as fm:
+            # Write module file
+            fm.write("module;\n\n")
+            
+            # Write global module fragment (from Headers to Declaration comment)
+            # Filter out 'using' declarations to avoid conflicts
+            if headers_start >= 0 and declaration_start >= 0:
+                for i in range(headers_start, declaration_start):
+                    line: str = lines[i]
+                    if 'using' not in line:
+                        fm.write(line)
+            
+            fm.write("\nexport module httplib;\n\n")
+            fm.write("export extern \"C++\" {\n")
+            fm.write(f"{' ' * 4}#include \"httplib.h\"\n")
+            fm.write("}\n")
+
+        print(f"Wrote {cppm_out}")
+    else:
+        print(f"{cppm_out} is up to date")
+
+
+if __name__ == "__main__":
+    main()

+ 15 - 11
split.py

@@ -7,15 +7,14 @@ import sys
 from argparse import ArgumentParser, Namespace
 from typing import List
 
+BORDER: str = '// ----------------------------------------------------------------------------'
 
 def main() -> None:
     """Main entry point for the script."""
-    BORDER: str = '// ----------------------------------------------------------------------------'
 
     args_parser: ArgumentParser = ArgumentParser(description=__doc__)
     args_parser.add_argument(
-        "-e", "--extension", help="extension of the implementation file (default: cc)",
-        default="cc"
+        "-e", "--extension", help="extension of the implementation file (default: cc)", default="cc"
     )
     args_parser.add_argument(
         "-o", "--out", help="where to write the files (default: out)", default="out"
@@ -25,14 +24,14 @@ def main() -> None:
     cur_dir: str = os.path.dirname(sys.argv[0])
     if not cur_dir:
         cur_dir = '.'
-    lib_name: str = 'httplib'
+    lib_name: str = "httplib"
     header_name: str = f"/{lib_name}.h"
     source_name: str = f"/{lib_name}.{args.extension}"
     # get the input file
-    in_file: str = cur_dir + header_name
+    in_file: str = f"{cur_dir}{header_name}"
     # get the output file
-    h_out: str = args.out + header_name
-    cc_out: str = args.out + source_name
+    h_out: str = f"{args.out}{header_name}"
+    cc_out: str = f"{args.out}{source_name}"
 
     # if the modification time of the out file is after the in file,
     # don't split (as it is already finished)
@@ -51,18 +50,23 @@ def main() -> None:
 
         in_implementation: bool = False
         cc_out: str = args.out + source_name
+        
         with open(h_out, 'w') as fh, open(cc_out, 'w') as fc:
-            fc.write('#include "httplib.h"\n')
-            fc.write('namespace httplib {\n')
+            # Write source file
+            fc.write("#include \"httplib.h\"\n")
+            fc.write("namespace httplib {\n")
+            
+            # Process lines for header and source split
             for line in lines:
                 is_border_line: bool = BORDER in line
                 if is_border_line:
                     in_implementation: bool = not in_implementation
                 elif in_implementation:
-                    fc.write(line.replace('inline ', ''))
+                    fc.write(line.replace("inline ", ""))
                 else:
                     fh.write(line)
-            fc.write('} // namespace httplib\n')
+            
+            fc.write("} // namespace httplib\n")
 
         print(f"Wrote {h_out} and {cc_out}")
     else: