Battle plan for dropping `HttpServer`


The HttpServer was added back when there was no stand-alone webserver for .NET. It was chosen because it was a small-ish project with a simple interface. Sadly it was never maintained, and I ended up “saving” it from oblivion when Codeplex was shut down.

The entire codebase in there is pretty typical of that era with very few things one would expect from a modern webserver (CORS, Domain, WebSockets, Dependency Injection, etc). @Jojo-1000 recently discovered that it even has its own (broken) implementation of UrlDecode.

I expect there to be many more problems hiding in various places. Fortunately some work was started by @tsuckow on creating a new webserver implementation using Kestrel and this was picked up by @npodbielski.

I have been looking at that combined work and I think it is important that this transition is done ASAP, so this post is an attempt to make a work plan for what should happen.

The current state is a mixed bag with some endpoints supported, some not, and most are wrapping existing functionality. My end-goal is to fully remove HttpServer and rely only on Kestrel, while providing a code-base that is familiar to ASP.NET developers.

I have made a map of all endpoints that needs to be implemented, so I will be deleting them and then copy back the code into the Kestrel functions, adapting as needed.

As always, input and help is much appreciated!

Proposed plan

My current overview of the task is listed below, and I will update as I progress:

  • Delete all references to HttpServer, including the endpoints
  • Get the Kestrel server starting
  • Introduce the previous commandline options and map them to Kestrel
  • Implement endpoints, update as needed
    • /api/v1/acknowledgements - Duplicati.Server.WebServer.RESTMethods.Acknowledgements
      • GET /api/v1/acknowledgements
    • /api/v1/backup - Duplicati.Server.WebServer.RESTMethods.Backup
      • GET /api/v1/backup
      • PUT /api/v1/backup
      • POST /api/v1/backup
      • DELETE /api/v1/backup
    • /api/v1/backupdefaults - Duplicati.Server.WebServer.RESTMethods.BackupDefaults
      • GET /api/v1/backupdefaults
    • /api/v1/backups - Duplicati.Server.WebServer.RESTMethods.Backups
      • GET /api/v1/backups
      • POST /api/v1/backups
    • /api/v1/bugreport - Duplicati.Server.WebServer.RESTMethods.BugReport
      • GET /api/v1/bugreport
    • /api/v1/captcha - Duplicati.Server.WebServer.RESTMethods.Captcha
      • GET /api/v1/captcha
      • POST /api/v1/captcha
    • /api/v1/changelog - Duplicati.Server.WebServer.RESTMethods.Changelog
      • GET /api/v1/changelog
    • /api/v1/commandline - Duplicati.Server.WebServer.RESTMethods.CommandLine
      • GET /api/v1/commandline
      • POST /api/v1/commandline
    • /api/v1/filesystem - Duplicati.Server.WebServer.RESTMethods.Filesystem
      • GET /api/v1/filesystem
      • POST /api/v1/filesystem
    • /api/v1/help - Duplicati.Server.WebServer.RESTMethods.Help
      • GET /api/v1/help
    • /api/v1/hyperv - Duplicati.Server.WebServer.RESTMethods.HyperV
      • GET /api/v1/hyperv
    • /api/v1/licenses - Duplicati.Server.WebServer.RESTMethods.Licenses
      • GET /api/v1/licenses
    • /api/v1/logdata - Duplicati.Server.WebServer.RESTMethods.LogData
      • GET /api/v1/logdata
    • /api/v1/mssql - Duplicati.Server.WebServer.RESTMethods.MSSQL
      • GET /api/v1/mssql
    • /api/v1/notification - Duplicati.Server.WebServer.RESTMethods.Notification
      • GET /api/v1/notification
      • DELETE /api/v1/notification
    • /api/v1/notifications - Duplicati.Server.WebServer.RESTMethods.Notifications
      • GET /api/v1/notifications
    • /api/v1/progressstate - Duplicati.Server.WebServer.RESTMethods.ProgressState
      • GET /api/v1/progressstate
    • /api/v1/remoteoperation - Duplicati.Server.WebServer.RESTMethods.RemoteOperation
      • GET /api/v1/remoteoperation
      • POST /api/v1/remoteoperation
    • /api/v1/serversetting - Duplicati.Server.WebServer.RESTMethods.ServerSetting
      • GET /api/v1/serversetting
      • PUT /api/v1/serversetting
    • /api/v1/serversettings - Duplicati.Server.WebServer.RESTMethods.ServerSettings
      • GET /api/v1/serversettings
      • PATCH /api/v1/serversettings
    • /api/v1/serverstate - Duplicati.Server.WebServer.RESTMethods.ServerState
      • GET /api/v1/serverstate
      • POST /api/v1/serverstate
    • /api/v1/systeminfo - Duplicati.Server.WebServer.RESTMethods.SystemInfo
      • GET /api/v1/systeminfo
    • /api/v1/systemwidesettings - Duplicati.Server.WebServer.RESTMethods.SystemWideSettings
      • GET /api/v1/systemwidesettings
    • /api/v1/tags - Duplicati.Server.WebServer.RESTMethods.Tags
      • GET /api/v1/tags
    • /api/v1/task - Duplicati.Server.WebServer.RESTMethods.Task
      • GET /api/v1/task
      • POST /api/v1/task
    • /api/v1/tasks - Duplicati.Server.WebServer.RESTMethods.Tasks
      • GET /api/v1/tasks
    • /api/v1/uisettings - Duplicati.Server.WebServer.RESTMethods.UISettings
      • GET /api/v1/uisettings
      • POST /api/v1/uisettings
      • PATCH /api/v1/uisettings
    • /api/v1/updates - Duplicati.Server.WebServer.RESTMethods.Updates
      • POST /api/v1/updates
    • /api/v1/webmodule - Duplicati.Server.WebServer.RESTMethods.WebModule
      • POST /api/v1/webmodule
    • /api/v1/webmodules - Duplicati.Server.WebServer.RESTMethods.WebModules
      • GET /api/v1/webmodules
  • Testing
  • Revisit login/auth/CSRF

To this point I just want to comment that right now even get requests require a csrf token. In the updated Angular implementation this isn’t supported by the built-in functions. Usually only non-safe methods (that modify state) have this protection. I would suggest to address that with this update.

GET requests can potentially leak CSRF tokens at several locations, such as the browser history, log files, network utilities that log the first line of a HTTP request, and Referer headers if the protected site links to an external site.

A current annoyance (to me) is that if I run the same web browser to two Duplicati on different ports, I get:

Failed to read backup defaults: Missing XSRF Token. Please reload the page

or maybe similar messages. I can stop this by putting one Duplicati on localhost and one on because that causes Edge (so presumably Chrome) to consider them different, having their own cookies.

This setup is a bit unusual currently, but I don’t know if any of the Duplicati Inc. work will get into it further…

That could be fixed for example by adding the port to the cookie name. Then they would still be shared, but each instance has its separate cookie. Maybe only if the default port is not used.

1 Like

This looks like it only applies for CSRF applied as query strings, not headers, which is recommended just below the quote:

Since requests with custom headers are automatically subject to the same-origin policy, it is more secure to insert the CSRF token in a custom HTTP request header via JavaScript than adding a CSRF token in the hidden field form parameter.

I agree and I think the whole setup has to be updated to better reflect best practices 2024.

Ok, so I made “some” progress: Remove httpserver by kenkendk · Pull Request #5227 · duplicati/duplicati · GitHub

I have now made an overhaul of the security of the webserver:

Some highlights:

  • Empty/no password is no longer permitted
  • Random password assigned on first start
    • Tray-icon can generate a signin token that grants access without knowing the password
    • Webserver will output a link with a signin token on startup
    • Both options can also use --webservice-password=... to (re-)set a password
  • Password is stored with PBKDF2 to prevent #5197
  • Using three levels of tokens, all based on JWT
    • SignIn: very short lifetime; only supports granting a login
    • Access: The bearer token that grants access to the API; short-lived, never persisted in browser
    • Refresh: A token that can be exchanged for an access token; long-lived, only works on a single endpoint; using token counters to prevent re-use
  • Removed XSRF as the system no longer relies on automatic credentials (no auth/session cookies)

The system now stores the encryption key for the JWT in the database, which means it is still vulnerable to the method described in #5197, but it does not expose the password. With the key it is possible to issue SignIn and Access tokens, and with some luck fake a refresh token.

I would like to fix this with a general approach to storing sensitive information in the database, so we can cover the remote destination credentials and other things as well.

I have added the method suggested by @Jojo-1000 with different cookie names for different host ports.

I have now gone through most of the functionality now, and it generally works.

There are still some cases where the parsing of arguments is slightly different and this causes “Direct restore” to fail.

@npodbielski I have temporarily disabled websockets because it was not correctly relaying all status messages, but the implementation looks really nice and simple.

Hey sorry for being silent about this. I had very busy period at work (whole May and half of June) and now I am on vacation till the end of June. Maybe I will be able to do something next week - I am not sure. It is finally nice whether so I have a lot of stuff to do in the garden also - it is better to spend some time in the sun instead of indoor watching computer screen :smiley:

I did not moved all of the longpolls to the websockets. Should work in parrallel - maybe I did made some mistake sorry about this.

Sounds amazing :sun_with_face:

I can just report what I see, without debugging further:

  • Browser “connects” and seems connected, but does not seem to send the connected event
  • The backend never triggers the connect and appears to have no connected sockets
  • Stopping the server triggers a disconnect event in the browser

I might have broken things while I was restructuring the code and adding authentication :confused: