changeset 6307:86b3ad58baa3

HttpBindAddresses
author Alain Mazy <am@orthanc.team>
date Thu, 11 Sep 2025 18:32:39 +0200
parents e6755569b9d2
children 1486ee6d0a70
files NEWS OrthancFramework/Resources/CodeGeneration/ErrorCodes.json OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py OrthancFramework/Sources/Enumerations.cpp OrthancFramework/Sources/Enumerations.h OrthancFramework/Sources/HttpServer/HttpServer.cpp OrthancFramework/Sources/HttpServer/HttpServer.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Resources/Configuration.json OrthancServer/Sources/main.cpp
diffstat 10 files changed, 65 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Sep 09 15:45:16 2025 +0200
+++ b/NEWS	Thu Sep 11 18:32:39 2025 +0200
@@ -1,6 +1,14 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* New configuration "HttpBindAddresses" to list the IP addresses on which the
+  HTTP server listens.  By default, this list is empty and the HTTP server listens
+  on all network interfaces.
+
+
 Maintenance
 -----------
 
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Thu Sep 11 18:32:39 2025 +0200
@@ -393,7 +393,7 @@
   {
     "Code": 2003, 
     "Name": "HttpPortInUse", 
-    "Description": "The TCP port of the HTTP server is privileged or already in use"
+    "Description": "The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist"
   },
   {
     "Code": 2004, 
--- a/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Thu Sep 11 18:32:39 2025 +0200
@@ -37,7 +37,7 @@
 ##
 
 with open(os.path.join(BASE, 'OrthancFramework', 'Resources', 'CodeGeneration', 'ErrorCodes.json'), 'r') as f:
-    ERRORS = json.loads(re.sub('/\*.*?\*/', '', f.read()))
+    ERRORS = json.loads(re.sub(r'/\*.*?\*/', '', f.read()))
 
 for error in ERRORS:
     if error['Code'] >= START_PLUGINS:
@@ -48,7 +48,7 @@
     a = f.read()
 
 HTTP = {}
-for i in re.findall('(HttpStatus_([0-9]+)_\w+)', a):
+for i in re.findall(r'(HttpStatus_([0-9]+)_\w+)', a):
     HTTP[int(i[1])] = i[0]
 
 
@@ -64,7 +64,7 @@
 s = ',\n'.join(map(lambda x: '    ErrorCode_%s = %d    /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS))
 
 s += ',\n    ErrorCode_START_PLUGINS = %d' % START_PLUGINS
-a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
+a = re.sub(r'(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
 
 with open(path, 'w') as f:
     f.write(a)
@@ -81,7 +81,7 @@
 
 s = ',\n'.join(map(lambda x: '    OrthancPluginErrorCode_%s = %d    /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS))
 s += ',\n\n    _OrthancPluginErrorCode_INTERNAL = 0x7fffffff\n  '
-a = re.sub('(typedef enum\s*{)[^}]*?(} OrthancPluginErrorCode;)', r'\1\n%s\2' % s, a, re.DOTALL)
+a = re.sub(r'(typedef enum\s*{)[^}]*?(} OrthancPluginErrorCode;)', r'\1\n%s\2' % s, a, re.DOTALL)
 
 with open(path, 'w') as f:
     f.write(a)
@@ -99,7 +99,7 @@
     a = f.read()
 
 s = '\n\n'.join(map(lambda x: '      case ErrorCode_%s:\n        return "%s";' % (x['Name'], x['Description']), ERRORS))
-a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+a = re.sub(r'(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
            r'\1\n%s\2' % s, a, re.DOTALL)
 
 def GetHttpStatus(x):
@@ -107,7 +107,7 @@
     return '      case ErrorCode_%s:\n        return %s;' % (x['Name'], s)
 
 s = '\n\n'.join(map(GetHttpStatus, filter(lambda x: 'HttpStatus' in x, ERRORS)))
-a = re.sub('(ConvertErrorCodeToHttpStatus\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+a = re.sub(r'(ConvertErrorCodeToHttpStatus\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
            r'\1\n%s\2' % s, a, re.DOTALL)
 
 with open(path, 'w') as f:
@@ -125,10 +125,10 @@
 
 e = list(filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS))
 s = ',\n'.join(map(lambda x: '      ErrorCode_%s' % x['Name'], e))
-a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
+a = re.sub(r'(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
 
 s = '\n\n'.join(map(lambda x: '          case ErrorCode_%s:\n            return "%s";' % (x['Name'], x['Description']), e))
-a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+a = re.sub(r'(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
            r'\1\n%s\2' % s, a, re.DOTALL)
 
 with open(path, 'w') as f:
@@ -145,7 +145,7 @@
     a = f.read()
 
 s = '\n'.join(map(lambda x: '    PrintErrorCode(ErrorCode_%s, "%s");' % (x['Name'], x['Description']), ERRORS))
-a = re.sub('(static void PrintErrors[^{}]*?{[^{}]*?{)([^}]*?)}', r'\1\n%s\n  }' % s, a, re.DOTALL)
+a = re.sub(r'(static void PrintErrors[^{}]*?{[^{}]*?{)([^}]*?)}', r'\1\n%s\n  }' % s, a, re.DOTALL)
 
 with open(path, 'w') as f:
     f.write(a)
--- a/OrthancFramework/Sources/Enumerations.cpp	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Thu Sep 11 18:32:39 2025 +0200
@@ -247,7 +247,7 @@
         return "The specified path does not point to a directory";
 
       case ErrorCode_HttpPortInUse:
-        return "The TCP port of the HTTP server is privileged or already in use";
+        return "The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist";
 
       case ErrorCode_DicomPortInUse:
         return "The TCP port of the DICOM server is privileged or already in use";
--- a/OrthancFramework/Sources/Enumerations.h	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Thu Sep 11 18:32:39 2025 +0200
@@ -192,7 +192,7 @@
     ErrorCode_DirectoryOverFile = 2000    /*!< The directory to be created is already occupied by a regular file */,
     ErrorCode_FileStorageCannotWrite = 2001    /*!< Unable to create a subdirectory or a file in the file storage */,
     ErrorCode_DirectoryExpected = 2002    /*!< The specified path does not point to a directory */,
-    ErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use */,
+    ErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist */,
     ErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is privileged or already in use */,
     ErrorCode_BadHttpStatusInRest = 2005    /*!< This HTTP status is not allowed in a REST API */,
     ErrorCode_RegularFileExpected = 2006    /*!< The specified path does not point to a regular file */,
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Thu Sep 11 18:32:39 2025 +0200
@@ -1732,6 +1732,11 @@
     return port_;
   }
 
+  void HttpServer::SetBindAddresses(const std::set<std::string>& bindAddresses)
+  {
+    bindAddresses_ = bindAddresses;
+  }
+
   void HttpServer::Start()
   {
     // reset thread counter used to generate HTTP thread names.
@@ -1758,11 +1763,27 @@
         port += "s";
       }
 
+      std::string listeningPorts;
+      if (bindAddresses_.size() == 0) // default behaviour till 1.12.9 and when no "HttpBindAddresses" configurations are provided
+      {
+        listeningPorts = port;
+      }
+      else
+      {
+        std::set<std::string> addresses;
+        for (std::set<std::string>::const_iterator it = bindAddresses_.begin(); it != bindAddresses_.end(); ++it)
+        {
+          addresses.insert(*it + ":" + port);
+        }
+
+        Toolbox::JoinStrings(listeningPorts, addresses, ",");
+      }
+
       std::vector<const char*> options;
 
       // Set the TCP port for the HTTP server
       options.push_back("listening_ports");
-      options.push_back(port.c_str());
+      options.push_back(listeningPorts.c_str());
         
       // Optimization reported by Chris Hafey
       // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ
@@ -1873,7 +1894,7 @@
         else
         {
           throw OrthancException(ErrorCode_HttpPortInUse,
-                                 " (port = " + boost::lexical_cast<std::string>(port_) + ")");
+                                 " (" + listeningPorts + ")");
         }
       }
 
@@ -1885,7 +1906,7 @@
       }
 #endif
 
-      CLOG(WARNING, HTTP) << "HTTP server listening on port: " << GetPortNumber()
+      CLOG(WARNING, HTTP) << "HTTP server listening on : " << listeningPorts
                           << " (HTTPS encryption is "
                           << (IsSslEnabled() ? "enabled" : "disabled")
                           << ", remote access is "
--- a/OrthancFramework/Sources/HttpServer/HttpServer.h	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.h	Thu Sep 11 18:32:39 2025 +0200
@@ -108,6 +108,7 @@
     bool sslHasCiphers_;
     std::string sslCiphers_;
     uint16_t port_;
+    std::set<std::string> bindAddresses_;
     IIncomingHttpRequestFilter* filter_;
     bool keepAlive_;
     unsigned int keepAliveTimeout_;
@@ -137,6 +138,8 @@
 
     uint16_t GetPortNumber() const;
 
+    void SetBindAddresses(const std::set<std::string>& bindAddresses);
+
     void Start();
 
     void Stop();
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Sep 11 18:32:39 2025 +0200
@@ -299,7 +299,7 @@
     OrthancPluginErrorCode_DirectoryOverFile = 2000    /*!< The directory to be created is already occupied by a regular file */,
     OrthancPluginErrorCode_FileStorageCannotWrite = 2001    /*!< Unable to create a subdirectory or a file in the file storage */,
     OrthancPluginErrorCode_DirectoryExpected = 2002    /*!< The specified path does not point to a directory */,
-    OrthancPluginErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use */,
+    OrthancPluginErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist */,
     OrthancPluginErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is privileged or already in use */,
     OrthancPluginErrorCode_BadHttpStatusInRest = 2005    /*!< This HTTP status is not allowed in a REST API */,
     OrthancPluginErrorCode_RegularFileExpected = 2006    /*!< The specified path does not point to a regular file */,
--- a/OrthancServer/Resources/Configuration.json	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancServer/Resources/Configuration.json	Thu Sep 11 18:32:39 2025 +0200
@@ -118,6 +118,19 @@
   // HTTP port for the REST services and for the GUI
   "HttpPort" : 8042,
 
+  // IP addresses the HTTP server listens on.  
+  // This is usefull when, e.g. your computer has multiple network
+  // interfaces and you want Orthanc to be accessible on a single
+  // sub-network.
+  // By default, the HTTP server listens on all IP addresses.
+  // Note that, setting "HttpBindAddresses" to ["127.0.0.1"] is
+  // almost equivalent to settting "RemoteAccessAllowed".  In the
+  // first case, external HTTP clients won't be able to connect at 
+  // all to Orthanc while, in the second case, external HTTP clients 
+  // will be able to connect but will receive a 401 Unauthorized
+  // HTTP status code. 
+  //"HttpBindAddresses": ["1.2.3.4", "127.0.0.1"]
+
   // When the following option is "true", if an error is encountered
   // while calling the REST API, a JSON message describing the error
   // is put in the HTTP answer. This feature can be disabled if the
--- a/OrthancServer/Sources/main.cpp	Tue Sep 09 15:45:16 2025 +0200
+++ b/OrthancServer/Sources/main.cpp	Thu Sep 11 18:32:39 2025 +0200
@@ -881,7 +881,7 @@
     PrintErrorCode(ErrorCode_DirectoryOverFile, "The directory to be created is already occupied by a regular file");
     PrintErrorCode(ErrorCode_FileStorageCannotWrite, "Unable to create a subdirectory or a file in the file storage");
     PrintErrorCode(ErrorCode_DirectoryExpected, "The specified path does not point to a directory");
-    PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is privileged or already in use");
+    PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist");
     PrintErrorCode(ErrorCode_DicomPortInUse, "The TCP port of the DICOM server is privileged or already in use");
     PrintErrorCode(ErrorCode_BadHttpStatusInRest, "This HTTP status is not allowed in a REST API");
     PrintErrorCode(ErrorCode_RegularFileExpected, "The specified path does not point to a regular file");
@@ -1073,6 +1073,9 @@
       // HTTP server
       httpServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpThreadsCount", 50));
       httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042));
+      std::set<std::string> httpBindAddresses;
+      lock.GetConfiguration().GetSetOfStringsParameter(httpBindAddresses, "HttpBindAddresses");
+      httpServer.SetBindAddresses(httpBindAddresses);
       httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
       httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive));
       httpServer.SetKeepAliveTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("KeepAliveTimeout", 1));