From c0ad54da88c933c13127dad7c26ca31d069f38d0 Mon Sep 17 00:00:00 2001 From: Doug Slater Date: Sun, 21 Dec 2025 08:26:38 -0500 Subject: [PATCH 1/3] Handle exe paths with spaces --- BrowseRouter/Model/Args.cs | 14 +- README.md | 657 +++++++++--------- .../ArgsTests/GetPathAndArgsMethod.cs | 30 + 3 files changed, 368 insertions(+), 333 deletions(-) diff --git a/BrowseRouter/Model/Args.cs b/BrowseRouter/Model/Args.cs index 17122d0..dde75dd 100644 --- a/BrowseRouter/Model/Args.cs +++ b/BrowseRouter/Model/Args.cs @@ -22,13 +22,19 @@ public static (string, string) SplitPathAndArgs(string s) return (path, args); } - // If not quoted, split on first space to separate path from args + // If not quoted, try to split on first space to separate path from args. + // But only if the remainder doesn't contain a backslash, which would indicate + // the first space is within the path itself (e.g., "C:\Program Files\..."). + // Users with paths containing spaces must quote the path. int spaceIndex = s.IndexOf(' '); if (spaceIndex > 0) { - string path = s[..spaceIndex]; - string args = s[(spaceIndex + 1)..]; - return (path, args); + string afterSpace = s[(spaceIndex + 1)..]; + if (!afterSpace.Contains('\\')) + { + string path = s[..spaceIndex]; + return (path, afterSpace); + } } // The single executable without any other arguments. diff --git a/README.md b/README.md index 059bd15..1a06f0b 100644 --- a/README.md +++ b/README.md @@ -1,329 +1,328 @@ - -# Sponsor - -BrowseRouter is sponsored by **[Linklever](https://linklever.net)**. - -Linklever is like BrowseRouter, but it's cross-platform and has a user interface. - -It's made by the same author, [@nref](https://github.com/nref/), so you can expect the same quality and support. - -**[Try Linklever today](https://linklever.net)** and save 25% with the code `BROWSEROUTER2025` - -| Feature | BrowseRouter | Linklever | -|-------------------------------------|---------------------------------------|--------------------------------------| -| Registers as default browser | ✅ | ✅ | -| Rules | ✅ | ✅ | -| Source Apps | ✅ | ✅ | -| Filters | ✅ | ✅ | -| Windows | ✅ | ✅ | -| macOS | ❌ | ✅ | -| Linux | ❌ | ✅ | -| Detects installed browsers | ❌ | ✅ | -| GUI | ❌ | ✅ | -| Browser extension | ❌ | ✅ | -| Configuration | config files | via GUI | - -# BrowseRouter - -

- -

- -In Windows, launch a different browser depending on the url. - -## Usage - -``` - BrowseRouter.exe [-h | --help] - Show help. - - BrowseRouter.exe - Automatic registration. - Same as --register if not already registered, otherwise --unregister. - If the app has moved or been renamed, updates the existing registration. - - BrowseRouter.exe [-r | --register] - Register as a web browser, then open Settings. - The user must choose BrowseRouter as the default browser. - No need to run as admin. - - BrowseRouter.exe [-u | --unregister] - Unregister as a web browser. - - BrowseRouter.exe https://example.org/ [...more URLs] - Launch one or more URLs -``` - -## Setting Up - -1. Download the latest release. -2. Open `config.json` and `filters.json` and customize as desired. -3. Run `BrowseRouter.exe` without arguments. No need to run as admin. - It will register with Windows as a web browser and open the Settings app. - To unregister, run it again. - If you later move BrowseRouter to a new folder, or rename the exe, run it again to update the registration. -4. In the settings app, set `BrowseRouter` as the default browser. - -Settings - -## Supported Platforms - -- Windows 10, x64 and arm64 -- Windows 11, x64 and arm64 - -## Why? - -`BrowseRouter` becomes your default "browser". When you click a link, it decides which real browser to launch. If you have multiple browsers installed, this is very useful. Example use cases: - -- *Compatibility*. Some sites you visit work better in specific browsers. You don't care which browser opens, just that the loaded page works. -- *Privacy*. For example, one browser is configured to use a proxy while another isn't. -- *Workplace*. You access an intranet site through a specific browser while you prefer to use another browser for the rest of the internet. -- *Browser wars*. You're tired of browsers jostling to be the default. You're tired of changing the default browser. - -## Security - -`BrowseRouter` is clean software. I, [@nref](https://github.com/nref/), rest my reputation on it. - -- The code is open-source and publicly reviewable. -- I provide pre-built binaries whichs are cryptographically signed with an Extended Validation code-signing certificate. What that means is I regularly navigate a lot of bureaucracy which keeps me legally accountable. Moreover, you can know that the binaries are from me and not modified by some middleman. -- If you don't trust the the binaries, you can build them from source. -- Windows Defender currently reports no problems, as of version 0.8.0.0 -- A small number of malware scanners are reporting false positives as of version 0.8.0.0. I cannot do much about these as the scanners do not provide specific complaints. - - [65/68 VirusTotal Report for x64](https://www.virustotal.com/gui/file/a83b01de823c3698af768d51903aa6d6cbbc04dc965e6626e748e877cdbb33a9) - - [66/68 VirusTotal Report for arm64](https://www.virustotal.com/gui/file/498b0a33f6508613a2e8c2592586dccad78d83e434542e0191cf8d28c900acfb) -- Possible reasons a scanner may not like BrowseRouter: - - It adds registry keys. It must in order to register as a browser. - - It launches other browsers. - -## Privacy - -BrowseRouter contains no tracking, and it makes no network connections of its own whatsoever. - -Your system administrator could know which pages you are visiting by auditing process start logs e.g. `BrowseRouter.exe http://some-naughty-site.example`. They would have the same information for any browser. - -## Notifications - -By default, `BrowseRouter` will show a desktop notification when it opens a link. You can disable this in `config.json`. - -Notification - -## Config - -**Important: `config.ini` is deprecated. Use `config.json`** - -For now, the app falls back to `config.ini` if `config.json` does not exist. This will be removed in a future version. - -Example `config.json`: - -```json -{ - "notify": { - "enabled": true - }, - "log": { - "enabled": true, - "comment": "Default file is C:\\Users\\\\AppData\\Local\\BrowseRouter\\yyyy-MM-dd.log" - "#file": "C:\\Users\\\\Desktop\\BrowseRouter.log" - }, - "browsers": { - "ff": "%ProgramFiles%\\Mozilla Firefox\\firefox.exe", - "chrome": "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", - "chromenw": "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe --new-window", - "edge": "%ProgramFiles(x86)%\\Microsoft\\Edge\\Application\\msedge.exe", - "opera": "%UserProfile%\\AppData\\Local\\Programs\\Opera\\opera.exe" - }, - "sources": { - "* - Notepad -> notepad": "chrome", - "Slack | Test*": "chromenw", - " -> AutoHotkey64": "ff" - }, - "urls": { - "*google.com": "chrome", - "*microsoft.com": "edge", - "*mozilla.org": "ff" - }, - "filtersFile": "filters.json" -} -``` - -Example `config.ini` file (old, deprecated): - -```ini -[notify] -# Show a desktop notification when opening a link. Defaults to true -enabled = true -# Should the windows notification sound be silenced when displaying it. Defaults to true -#silent = false - -[log] -# Write log entries to a file. Defaults to false -enabled = true -# Default file is C:\Users\\AppData\Local\BrowseRouter\yyyy-MM-dd.log -#file = "C:\Users\\Desktop\BrowseRouter.log" - -# Default browser is first in list -# Use `{url}` to specify UWP app browser details (not currently working, see following issue: https://github.com/nref/BrowseRouter/issues/10) -# Environment variables (like %ProgramFiles% for example) can be used -[browsers] -ff = %ProgramFiles%\Mozilla Firefox\firefox.exe -# Open in a new window -chrome = C:\Program Files (x86)\Google\Chrome\Application\chrome.exe -chromenw = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --new-window -edge = %ProgramFiles(x86)%\Microsoft\Edge\Application\msedge.exe -opera = %UserProfile%\AppData\Local\Programs\Opera\opera.exe - -# Source preferences. -# - Only * is treated as a special character (wildcard). -# - Will take precedence over any URL preferences. -# - Matches on window title and specific process of the application used to open the link, like so "WindowTitle -> ProcessName". -[sources] -* - Notepad -> notepad = chrome -Slack | Test* = chromenw -# Source with no window (background processes) - -> AutoHotkey64 = ff -# Default case. Added automatically -# * = ff - -# Url preferences. -# - Only * is treated as a special character (wildcard). -# - Only domains are matched. Don't include protocols e.g. "https://" or paths e.g. "/some/path?query=value" -# - Be aware that subdomains don't match automatically, e.g. "youtube.com = chrome" would not launch Chrome for "www.youtube.com" -# For that reason, you'll often want a leading "*." e.g. "*.youtube.com". -# Note that "*youtube.com" would also match e.g. "notyoutube.com". -[urls] -*google.com = chrome -*microsoft.com = edge -*mozilla.org = ff - -[filters] -file = filters.json -``` - -### Browsers - -- Browsers must either be : - - fully-qualified paths to the executable e.g. `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`. - - full path to the executable with environment variable e.g. `%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe`. - - in your PATH environment variable (you will just have to set the name of the .exe then) e.g. `chrome.exe` with `%ProgramFiles(x86)%\Google\Chrome\Application` added to the PATH variable. -- Arguments are optional. However, if you provide arguments the path _must_ be enclosed in quotes. For example, `"chrome.exe" --new-window` -- If there are no arguments, then the paths do not need to be quoted. For example, `chrome.exe` will work. - -By default the URL to open is added as the last argument after the call to the executable. -But if you want it to be called differently, or only partially, you can use specific tags in the arguments you provide. -These tag will be replaced by their corresponding value in the URL : -- `{url}` the full, untruncated URL -- `{userinfo}` the userinfo part of the URL, might be blank if not present in the URL -- `{host}` the host of the URL, most often this will be a domain name (subdomain included) -- `{port}` the specific port of the URL, might be blank if not present in the URL -- `{authority}` the combination of userinfo, host and port separated by their respective delimiters if needed -- `{path}` the path of the URL, might be only `/` if the link targets the root of the domain -- `{query}` the query of the URL with the leading `?`, might be blank if not present in the URL -- `{fragment}` the fragment of the URL with the leading `#`, might be blank if not present in the URL - -For example if you want a browser which strip the query from the opened links, you can add this line: -`noQueryFF = "firefox.exe" "{authority}{path}{fragment}"` - -[More details and example about URI composition is available here!](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Example_URIs) - -### Sources - -- You can specify a "source preference" which matches the window title and the process name of the application used to open the link. - - For example, with this in the previous example `config.json`: - - ```json - "sources": { - "* - Notepad -> notepad": "ff", - "Slack | Test*": "chrome", - " -> AutoHotkey64": "ff" - } - ``` - - Then clicking a link in Notepad (end of the windows title ending with " - Notepad" with the process named "notepad") will open the link in Firefox, regardless of the URL. - -- Wildcards and full regular expressions may be used to match source window titles and process name the same way urls are. [See Urls section](#Urls). - -- Sources preferences takes precedence over all URLs preferences, so in the case of a conflict between a source preference and a URL preference, the source preference wins. - -### Urls - -There are two ways to specify an Url. You can use simple wildcards or full regular expressions. - -**Simple wildcards:** - - "microsoft.com": "ie" - "*.microsoft.com": "ie" - -- Only `*` is treated as a special character in URL patterns, and matches any characters (equivalent to the `.*` regex syntax). -- Only the domain part (or IP address) of a URL is checked. -- There is no implied wildcard at the start or end, so you must include these if you need them, but be aware that "microsoft.\*" will not only match "microsoft.com" and "microsoft.co.uk" but also "microsoft.somethingelse.com". - -**Full regular expressions:** - -```regex - "/sites\.google\.com/a/myproject.live\.com/": "chrome" -``` -- Full regular expressions are specified by wrapping it in /'s. -- The domain _and_ path are used in the Url comparison. -- The regular expression syntax is based on the Microsoft .NET implementation. - -## Filters - -BrowseRouter can filter URLs before sending them to the browser. This is useful e.g. to remove tracking or deobfuscate URLs. - -- If a URL is filtered, it says so in the notification and log. - - ![image](https://github.com/user-attachments/assets/f234089e-4b7c-4e60-a6f6-2813675614d5) - - ```2025-06-14 22:18:07 BrowseRouter: Filtered URL: example.org -> example.com``` - -- Filters are defined in `filters.json`. Filters consist of a Find regex pattern and Replace template pattern. In the case of multiple matches, `priority` determines which pattern is used. Note that `priority` is inverted, e.g. `1` is takes precedences over `2`. - -```json -[ - { - "name": "change org to com", - "find": "(.*)org(.*)", - "_comment": "Below, the replacement is the first capture group + com + second capture group" - "replace": "$1com$2", - "priority": 4 - } -] -``` - -- There is a special macro `unescape(...)` to un-urlescape obfuscated URLs before sending them to the browser. This is useful e.g. for Teams SafeLinks or Outlook URL Protection. It's not strictly necessary, since the browser unescapes the URL anyway, but it cleans up the notification. - -```json -[ - { - "name": "Bypass Teams Safelinks", - "find": ".*teams\\.cdn\\.office\\.net.*url=([^&]+).*", - "replace": "unescape($1)", - "priority": 2 - } -] -``` - -See `filters.json` for more examples. - -## Logs - -Logs are stored by default in `%localappdata%/BrowseRouter/`. For example, if you user name is `joe`, then the logs will be in `C:\Users\joe\AppData\Local\BrowseRouter\`. - -You can change the directory in the `log` section of `config.json`. - -You can enable disable or log files by setting `"enabled": "true"` or `false` in the `log` section of `config.json`. -If `enabled` is missing or doesn't equal `true`, logs will not be written. - -Log entries are also written to the console and can be seen if e.g. if launched from Command Prompt, PowerShell, or Windows Terminal. - -## Credit - -This is a fork of [BrowserSelector](https://github.com/DanTup/BrowserSelector/). That version is no longer mantained. This version carries on the vision, fixing bugs and adding new features. - -## Support - -If you like BrowseRouter, let me know in [a discussion](https://github.com/nref/BrowseRouter/discussions/new?category=general). BrowseRouter is just a hobby. You can help support continued development by "buying me a coffee." - -Buy Me A Coffee - -You can help determine what happens next with BrowseRouter by filling out [this survey](https://forms.gle/Bh5z472CZUN6qdon9). + +# Sponsor + +BrowseRouter is sponsored by **[Linklever](https://linklever.net)**. + +Linklever is like BrowseRouter, but it's cross-platform and has a user interface. + +It's made by the same author, [@nref](https://github.com/nref/), so you can expect the same quality and support. + +**[Try Linklever today](https://linklever.net)** and save 25% with the code `BROWSEROUTER2025` + +| Feature | BrowseRouter | Linklever | +|-------------------------------------|---------------------------------------|--------------------------------------| +| Registers as default browser | ✅ | ✅ | +| Rules | ✅ | ✅ | +| Source Apps | ✅ | ✅ | +| Filters | ✅ | ✅ | +| Windows | ✅ | ✅ | +| macOS | ❌ | ✅ | +| Linux | ❌ | ✅ | +| Detects installed browsers | ❌ | ✅ | +| GUI | ❌ | ✅ | +| Browser extension | ❌ | ✅ | +| Configuration | config files | via GUI | + +# BrowseRouter + +

+ +

+ +In Windows, launch a different browser depending on the url. + +## Usage + +``` + BrowseRouter.exe [-h | --help] + Show help. + + BrowseRouter.exe + Automatic registration. + Same as --register if not already registered, otherwise --unregister. + If the app has moved or been renamed, updates the existing registration. + + BrowseRouter.exe [-r | --register] + Register as a web browser, then open Settings. + The user must choose BrowseRouter as the default browser. + No need to run as admin. + + BrowseRouter.exe [-u | --unregister] + Unregister as a web browser. + + BrowseRouter.exe https://example.org/ [...more URLs] + Launch one or more URLs +``` + +## Setting Up + +1. Download the latest release. +2. Open `config.json` and `filters.json` and customize as desired. +3. Run `BrowseRouter.exe` without arguments. No need to run as admin. + It will register with Windows as a web browser and open the Settings app. + To unregister, run it again. + If you later move BrowseRouter to a new folder, or rename the exe, run it again to update the registration. +4. In the settings app, set `BrowseRouter` as the default browser. + +Settings + +## Supported Platforms + +- Windows 10, x64 and arm64 +- Windows 11, x64 and arm64 + +## Why? + +`BrowseRouter` becomes your default "browser". When you click a link, it decides which real browser to launch. If you have multiple browsers installed, this is very useful. Example use cases: + +- *Compatibility*. Some sites you visit work better in specific browsers. You don't care which browser opens, just that the loaded page works. +- *Privacy*. For example, one browser is configured to use a proxy while another isn't. +- *Workplace*. You access an intranet site through a specific browser while you prefer to use another browser for the rest of the internet. +- *Browser wars*. You're tired of browsers jostling to be the default. You're tired of changing the default browser. + +## Security + +`BrowseRouter` is clean software. I, [@nref](https://github.com/nref/), rest my reputation on it. + +- The code is open-source and publicly reviewable. +- I provide pre-built binaries whichs are cryptographically signed with an Extended Validation code-signing certificate. What that means is I regularly navigate a lot of bureaucracy which keeps me legally accountable. Moreover, you can know that the binaries are from me and not modified by some middleman. +- If you don't trust the the binaries, you can build them from source. +- Windows Defender currently reports no problems, as of version 0.8.0.0 +- A small number of malware scanners are reporting false positives as of version 0.8.0.0. I cannot do much about these as the scanners do not provide specific complaints. + - [65/68 VirusTotal Report for x64](https://www.virustotal.com/gui/file/a83b01de823c3698af768d51903aa6d6cbbc04dc965e6626e748e877cdbb33a9) + - [66/68 VirusTotal Report for arm64](https://www.virustotal.com/gui/file/498b0a33f6508613a2e8c2592586dccad78d83e434542e0191cf8d28c900acfb) +- Possible reasons a scanner may not like BrowseRouter: + - It adds registry keys. It must in order to register as a browser. + - It launches other browsers. + +## Privacy + +BrowseRouter contains no tracking, and it makes no network connections of its own whatsoever. + +Your system administrator could know which pages you are visiting by auditing process start logs e.g. `BrowseRouter.exe http://some-naughty-site.example`. They would have the same information for any browser. + +## Notifications + +By default, `BrowseRouter` will show a desktop notification when it opens a link. You can disable this in `config.json`. + +Notification + +## Config + +**Important: `config.ini` is deprecated. Use `config.json`** + +For now, the app falls back to `config.ini` if `config.json` does not exist. This will be removed in a future version. + +Example `config.json`: + +```json +{ + "notify": { + "enabled": true + }, + "log": { + "enabled": true, + "comment": "Default file is C:\\Users\\\\AppData\\Local\\BrowseRouter\\yyyy-MM-dd.log" + "#file": "C:\\Users\\\\Desktop\\BrowseRouter.log" + }, + "browsers": { + "ff": "%ProgramFiles%\\Mozilla Firefox\\firefox.exe", + "chrome": "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", + "chromenw": "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe --new-window", + "edge": "%ProgramFiles(x86)%\\Microsoft\\Edge\\Application\\msedge.exe", + "opera": "%UserProfile%\\AppData\\Local\\Programs\\Opera\\opera.exe" + }, + "sources": { + "* - Notepad -> notepad": "chrome", + "Slack | Test*": "chromenw", + " -> AutoHotkey64": "ff" + }, + "urls": { + "*google.com": "chrome", + "*microsoft.com": "edge", + "*mozilla.org": "ff" + }, + "filtersFile": "filters.json" +} +``` + +Example `config.ini` file (old, deprecated): + +```ini +[notify] +# Show a desktop notification when opening a link. Defaults to true +enabled = true +# Should the windows notification sound be silenced when displaying it. Defaults to true +#silent = false + +[log] +# Write log entries to a file. Defaults to false +enabled = true +# Default file is C:\Users\\AppData\Local\BrowseRouter\yyyy-MM-dd.log +#file = "C:\Users\\Desktop\BrowseRouter.log" + +# Default browser is first in list +# Use `{url}` to specify UWP app browser details (not currently working, see following issue: https://github.com/nref/BrowseRouter/issues/10) +# Environment variables (like %ProgramFiles% for example) can be used +[browsers] +ff = %ProgramFiles%\Mozilla Firefox\firefox.exe +# Open in a new window +chrome = C:\Program Files (x86)\Google\Chrome\Application\chrome.exe +chromenw = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --new-window +edge = %ProgramFiles(x86)%\Microsoft\Edge\Application\msedge.exe +opera = %UserProfile%\AppData\Local\Programs\Opera\opera.exe + +# Source preferences. +# - Only * is treated as a special character (wildcard). +# - Will take precedence over any URL preferences. +# - Matches on window title and specific process of the application used to open the link, like so "WindowTitle -> ProcessName". +[sources] +* - Notepad -> notepad = chrome +Slack | Test* = chromenw +# Source with no window (background processes) + -> AutoHotkey64 = ff +# Default case. Added automatically +# * = ff + +# Url preferences. +# - Only * is treated as a special character (wildcard). +# - Only domains are matched. Don't include protocols e.g. "https://" or paths e.g. "/some/path?query=value" +# - Be aware that subdomains don't match automatically, e.g. "youtube.com = chrome" would not launch Chrome for "www.youtube.com" +# For that reason, you'll often want a leading "*." e.g. "*.youtube.com". +# Note that "*youtube.com" would also match e.g. "notyoutube.com". +[urls] +*google.com = chrome +*microsoft.com = edge +*mozilla.org = ff + +[filters] +file = filters.json +``` + +### Browsers + +- Browsers must either be : + - fully-qualified paths to the executable e.g. `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`. + - full path to the executable with environment variable e.g. `%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe`. + - in your PATH environment variable (you will just have to set the name of the .exe then) e.g. `chrome.exe` with `%ProgramFiles(x86)%\Google\Chrome\Application` added to the PATH variable. +- Arguments are optional. However, if you provide arguments the path _must_ be enclosed in quotes. For example, `"chrome.exe" --new-window` + +By default the URL to open is added as the last argument after the call to the executable. +But if you want it to be called differently, or only partially, you can use specific tags in the arguments you provide. +These tag will be replaced by their corresponding value in the URL : +- `{url}` the full, untruncated URL +- `{userinfo}` the userinfo part of the URL, might be blank if not present in the URL +- `{host}` the host of the URL, most often this will be a domain name (subdomain included) +- `{port}` the specific port of the URL, might be blank if not present in the URL +- `{authority}` the combination of userinfo, host and port separated by their respective delimiters if needed +- `{path}` the path of the URL, might be only `/` if the link targets the root of the domain +- `{query}` the query of the URL with the leading `?`, might be blank if not present in the URL +- `{fragment}` the fragment of the URL with the leading `#`, might be blank if not present in the URL + +For example if you want a browser which strip the query from the opened links, you can add this line: +`noQueryFF = "firefox.exe" "{authority}{path}{fragment}"` + +[More details and example about URI composition is available here!](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Example_URIs) + +### Sources + +- You can specify a "source preference" which matches the window title and the process name of the application used to open the link. + - For example, with this in the previous example `config.json`: + + ```json + "sources": { + "* - Notepad -> notepad": "ff", + "Slack | Test*": "chrome", + " -> AutoHotkey64": "ff" + } + ``` + + Then clicking a link in Notepad (end of the windows title ending with " - Notepad" with the process named "notepad") will open the link in Firefox, regardless of the URL. + +- Wildcards and full regular expressions may be used to match source window titles and process name the same way urls are. [See Urls section](#Urls). + +- Sources preferences takes precedence over all URLs preferences, so in the case of a conflict between a source preference and a URL preference, the source preference wins. + +### Urls + +There are two ways to specify an Url. You can use simple wildcards or full regular expressions. + +**Simple wildcards:** + + "microsoft.com": "ie" + "*.microsoft.com": "ie" + +- Only `*` is treated as a special character in URL patterns, and matches any characters (equivalent to the `.*` regex syntax). +- Only the domain part (or IP address) of a URL is checked. +- There is no implied wildcard at the start or end, so you must include these if you need them, but be aware that "microsoft.\*" will not only match "microsoft.com" and "microsoft.co.uk" but also "microsoft.somethingelse.com". + +**Full regular expressions:** + +```regex + "/sites\.google\.com/a/myproject.live\.com/": "chrome" +``` +- Full regular expressions are specified by wrapping it in /'s. +- The domain _and_ path are used in the Url comparison. +- The regular expression syntax is based on the Microsoft .NET implementation. + +## Filters + +BrowseRouter can filter URLs before sending them to the browser. This is useful e.g. to remove tracking or deobfuscate URLs. + +- If a URL is filtered, it says so in the notification and log. + + ![image](https://github.com/user-attachments/assets/f234089e-4b7c-4e60-a6f6-2813675614d5) + + ```2025-06-14 22:18:07 BrowseRouter: Filtered URL: example.org -> example.com``` + +- Filters are defined in `filters.json`. Filters consist of a Find regex pattern and Replace template pattern. In the case of multiple matches, `priority` determines which pattern is used. Note that `priority` is inverted, e.g. `1` is takes precedences over `2`. + +```json +[ + { + "name": "change org to com", + "find": "(.*)org(.*)", + "_comment": "Below, the replacement is the first capture group + com + second capture group" + "replace": "$1com$2", + "priority": 4 + } +] +``` + +- There is a special macro `unescape(...)` to un-urlescape obfuscated URLs before sending them to the browser. This is useful e.g. for Teams SafeLinks or Outlook URL Protection. It's not strictly necessary, since the browser unescapes the URL anyway, but it cleans up the notification. + +```json +[ + { + "name": "Bypass Teams Safelinks", + "find": ".*teams\\.cdn\\.office\\.net.*url=([^&]+).*", + "replace": "unescape($1)", + "priority": 2 + } +] +``` + +See `filters.json` for more examples. + +## Logs + +Logs are stored by default in `%localappdata%/BrowseRouter/`. For example, if you user name is `joe`, then the logs will be in `C:\Users\joe\AppData\Local\BrowseRouter\`. + +You can change the directory in the `log` section of `config.json`. + +You can enable disable or log files by setting `"enabled": "true"` or `false` in the `log` section of `config.json`. +If `enabled` is missing or doesn't equal `true`, logs will not be written. + +Log entries are also written to the console and can be seen if e.g. if launched from Command Prompt, PowerShell, or Windows Terminal. + +## Credit + +This is a fork of [BrowserSelector](https://github.com/DanTup/BrowserSelector/). That version is no longer mantained. This version carries on the vision, fixing bugs and adding new features. + +## Support + +If you like BrowseRouter, let me know in [a discussion](https://github.com/nref/BrowseRouter/discussions/new?category=general). BrowseRouter is just a hobby. You can help support continued development by "buying me a coffee." + +Buy Me A Coffee + +You can help determine what happens next with BrowseRouter by filling out [this survey](https://forms.gle/Bh5z472CZUN6qdon9). diff --git a/Tests/BrowseRouter.Tests/ArgsTests/GetPathAndArgsMethod.cs b/Tests/BrowseRouter.Tests/ArgsTests/GetPathAndArgsMethod.cs index 7b21adc..0af0f4f 100644 --- a/Tests/BrowseRouter.Tests/ArgsTests/GetPathAndArgsMethod.cs +++ b/Tests/BrowseRouter.Tests/ArgsTests/GetPathAndArgsMethod.cs @@ -82,4 +82,34 @@ public void SplitsUnquotedPathWithArgs_PreservesMultipleArgs() // Assert result.Should().Be(("firefox.exe", "--new-window --url {url}")); } + + [Fact] + public void DoesNotSplitUnquotedPath_WhenPathContainsSpaces() + { + // Arrange - Issue #94 follow-up: Paths with spaces should not be split + // e.g., "C:\Program Files\Mozilla Firefox\firefox.exe arg" should NOT split on first space + // because that would break the path into "C:\Program" and "Files\Mozilla..." + string input = @"C:\Program Files\Mozilla Firefox\firefox.exe ext+container:name=Work&url={url}"; + + // Act + var result = Args.SplitPathAndArgs(input); + + // Assert - Should NOT split because the path contains spaces + // Users with paths containing spaces must quote the path + result.Should().Be((input, "")); + } + + [Fact] + public void SplitsQuotedPathWithSpaces_AndArgs() + { + // Arrange - Correct way to specify paths with spaces: use quotes + string input = @"""C:\Program Files\Mozilla Firefox\firefox.exe"" ext+container:name=Work&url={url}"; + + // Act + var result = Args.SplitPathAndArgs(input); + + // Assert + result.Should().Be((@"C:\Program Files\Mozilla Firefox\firefox.exe", "ext+container:name=Work&url={url}")); + } + } \ No newline at end of file From 45f5a50d1f06c1f5ccf78776bdca952ae55fb287 Mon Sep 17 00:00:00 2001 From: Doug Slater Date: Sun, 21 Dec 2025 09:06:34 -0800 Subject: [PATCH 2/3] Revert changes in last PR --- .../Config/UrlPreferenceExtensions.cs | 6 +-- BrowseRouter/Model/Args.cs | 15 ------ BrowseRouter/Properties/launchSettings.json | 4 ++ BrowseRouter/Services/ConfigService.cs | 2 +- BrowseRouter/config.json | 4 +- .../ArgsTests/GetPathAndArgsMethod.cs | 46 +------------------ .../Services/BrowserServiceTests.cs | 35 ++++---------- 7 files changed, 20 insertions(+), 92 deletions(-) diff --git a/BrowseRouter/Config/UrlPreferenceExtensions.cs b/BrowseRouter/Config/UrlPreferenceExtensions.cs index 927cdf5..09986f7 100644 --- a/BrowseRouter/Config/UrlPreferenceExtensions.cs +++ b/BrowseRouter/Config/UrlPreferenceExtensions.cs @@ -21,7 +21,7 @@ public static (string, string) GetDomainAndPattern(this UrlPreference pref, Uri if (urlPattern.StartsWith('/') && urlPattern.EndsWith('/')) { - // The domain from the INI file is a regex + // The domain from config is a regex string domain = uri.Authority + uri.AbsolutePath; string pattern = urlPattern.Substring(1, urlPattern.Length - 2); @@ -30,7 +30,7 @@ public static (string, string) GetDomainAndPattern(this UrlPreference pref, Uri if (urlPattern.StartsWith('?') && urlPattern.EndsWith('?')) { - // The domain from the INI file is a query filter + // The domain from config is a query filter string domain = uri.Authority + uri.PathAndQuery; string pattern = urlPattern.Substring(1, urlPattern.Length - 2); @@ -74,7 +74,7 @@ public static (string, string) GetDomainAndPattern(this UrlPreference pref, stri if (urlPattern.StartsWith('/') && urlPattern.EndsWith('/')) { - // The window title from the INI file is a regex + // The window title from config is a regex string pattern = urlPattern.Substring(1, urlPattern.Length - 2); return (windowTitle, pattern); diff --git a/BrowseRouter/Model/Args.cs b/BrowseRouter/Model/Args.cs index dde75dd..b630cc0 100644 --- a/BrowseRouter/Model/Args.cs +++ b/BrowseRouter/Model/Args.cs @@ -22,21 +22,6 @@ public static (string, string) SplitPathAndArgs(string s) return (path, args); } - // If not quoted, try to split on first space to separate path from args. - // But only if the remainder doesn't contain a backslash, which would indicate - // the first space is within the path itself (e.g., "C:\Program Files\..."). - // Users with paths containing spaces must quote the path. - int spaceIndex = s.IndexOf(' '); - if (spaceIndex > 0) - { - string afterSpace = s[(spaceIndex + 1)..]; - if (!afterSpace.Contains('\\')) - { - string path = s[..spaceIndex]; - return (path, afterSpace); - } - } - // The single executable without any other arguments. return (s, ""); } diff --git a/BrowseRouter/Properties/launchSettings.json b/BrowseRouter/Properties/launchSettings.json index 3994335..fa42121 100644 --- a/BrowseRouter/Properties/launchSettings.json +++ b/BrowseRouter/Properties/launchSettings.json @@ -38,6 +38,10 @@ "open multiple URLs": { "commandName": "Project", "commandLineArgs": "example.org example.com" + }, + "firefox container": { + "commandName": "Project", + "commandLineArgs": "https://www.work.test/foobar" } } } \ No newline at end of file diff --git a/BrowseRouter/Services/ConfigService.cs b/BrowseRouter/Services/ConfigService.cs index bf7668b..f4346df 100644 --- a/BrowseRouter/Services/ConfigService.cs +++ b/BrowseRouter/Services/ConfigService.cs @@ -56,7 +56,7 @@ public IEnumerable GetUrlPreferences(ConfigType configType) _ => [], }; - public async Task> GetFiltersAsync() + public virtual async Task> GetFiltersAsync() { if (string.IsNullOrWhiteSpace(config.FiltersFile)) return []; diff --git a/BrowseRouter/config.json b/BrowseRouter/config.json index d1fd3f0..a948c15 100644 --- a/BrowseRouter/config.json +++ b/BrowseRouter/config.json @@ -10,12 +10,14 @@ "urls": { "*google.com": "chrome", "*microsoft.com": "edge", - "*mozilla.org": "ff" + "*mozilla.org": "ff", + "*work.test*" : "ff-work" }, "browsers": { "edge": "%ProgramFiles(x86)%\\Microsoft\\Edge\\Application\\msedge.exe", "opera": "%UserProfile%\\AppData\\Local\\Programs\\Opera\\opera.exe", "ff": "%ProgramFiles%\\Mozilla Firefox\\firefox.exe", + "ff-work": "\"%ProgramFiles%\\Mozilla Firefox\\firefox.exe\" ext+container:name=Work&url={url}", "chrome": "%ProgramFiles%\\Google\\Chrome\\Application\\chrome.exe" }, "sources": { diff --git a/Tests/BrowseRouter.Tests/ArgsTests/GetPathAndArgsMethod.cs b/Tests/BrowseRouter.Tests/ArgsTests/GetPathAndArgsMethod.cs index 0af0f4f..c07e6a7 100644 --- a/Tests/BrowseRouter.Tests/ArgsTests/GetPathAndArgsMethod.cs +++ b/Tests/BrowseRouter.Tests/ArgsTests/GetPathAndArgsMethod.cs @@ -56,53 +56,10 @@ public void ReturnsInputAsPath_WhenInputContainsInvalidQuotes() result.Should().Be((input, "")); } - [Fact] - public void SplitsUnquotedPathWithArgs_OnFirstSpace() - { - // Arrange - Issue #94: User wants to pass args with {url} tag to browser - // e.g., "firefox.exe ext+container:name=Work&url={url}" - string input = "firefox.exe ext+container:name=Work&url={url}"; - - // Act - var result = Args.SplitPathAndArgs(input); - - // Assert - Should split on first space, treating rest as args - result.Should().Be(("firefox.exe", "ext+container:name=Work&url={url}")); - } - - [Fact] - public void SplitsUnquotedPathWithArgs_PreservesMultipleArgs() - { - // Arrange - string input = "firefox.exe --new-window --url {url}"; - - // Act - var result = Args.SplitPathAndArgs(input); - - // Assert - result.Should().Be(("firefox.exe", "--new-window --url {url}")); - } - - [Fact] - public void DoesNotSplitUnquotedPath_WhenPathContainsSpaces() - { - // Arrange - Issue #94 follow-up: Paths with spaces should not be split - // e.g., "C:\Program Files\Mozilla Firefox\firefox.exe arg" should NOT split on first space - // because that would break the path into "C:\Program" and "Files\Mozilla..." - string input = @"C:\Program Files\Mozilla Firefox\firefox.exe ext+container:name=Work&url={url}"; - - // Act - var result = Args.SplitPathAndArgs(input); - - // Assert - Should NOT split because the path contains spaces - // Users with paths containing spaces must quote the path - result.Should().Be((input, "")); - } - [Fact] public void SplitsQuotedPathWithSpaces_AndArgs() { - // Arrange - Correct way to specify paths with spaces: use quotes + // Arrange string input = @"""C:\Program Files\Mozilla Firefox\firefox.exe"" ext+container:name=Work&url={url}"; // Act @@ -111,5 +68,4 @@ public void SplitsQuotedPathWithSpaces_AndArgs() // Assert result.Should().Be((@"C:\Program Files\Mozilla Firefox\firefox.exe", "ext+container:name=Work&url={url}")); } - } \ No newline at end of file diff --git a/Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs b/Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs index 1cba065..6895388 100644 --- a/Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs +++ b/Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs @@ -36,45 +36,26 @@ public async Task HandlesUrl(string url) } [Fact] - public async Task SubstitutesUrlTagInUnquotedBrowserArgs() - { - // Issue #94: User configures browser with args containing {url} tag - // e.g., for Firefox containers: "firefox.exe ext+container:name=Work&url={url}" + public async Task RedirectsToFirefoxContainer() + { var config = BrowseRouter.Config.Config.Empty with { - Browsers = new Dictionary + Urls = new Dictionary { - ["ff-work"] = "firefox.exe ext+container:name=Work&url={url}", + ["*work.test*"] = "ff-work", }, - }; - CatchAllConfig.AddTo(config); - - var spy = new SpyProcessService(); - await new BrowserService(new ConfigService(config), new EmptyNotifyService(), spy) - .LaunchAsync("https://example.com/path", "Fake Window"); - - spy.LastPath.Should().Be("firefox.exe"); - spy.LastArgs.Should().Be("ext+container:name=Work&url=https://example.com/path"); - } - - [Fact] - public async Task SubstitutesUrlTagInQuotedBrowserArgs() - { - // When browser path is quoted, args with {url} should still work - var config = BrowseRouter.Config.Config.Empty with - { Browsers = new Dictionary { - ["ff-work"] = "\"firefox.exe\" ext+container:name=Work&url={url}", + ["ff-work"] = "\"%ProgramFiles%\\Mozilla Firefox\\firefox.exe\" ext+container:name=Work&url={url}", }, }; CatchAllConfig.AddTo(config); var spy = new SpyProcessService(); await new BrowserService(new ConfigService(config), new EmptyNotifyService(), spy) - .LaunchAsync("https://example.com/path", "Fake Window"); + .LaunchAsync("https://www.work.test/foobar", "Fake Window"); - spy.LastPath.Should().Be("firefox.exe"); - spy.LastArgs.Should().Be("ext+container:name=Work&url=https://example.com/path"); + spy.LastPath.Should().Be("C:\\Program Files\\Mozilla Firefox\\firefox.exe"); + spy.LastArgs.Should().Be("ext+container:name=Work&url=https://www.work.test/foobar"); } } \ No newline at end of file From 71bb0bde4454cedb838cf145a99db104d162f6ff Mon Sep 17 00:00:00 2001 From: Doug Slater Date: Sun, 21 Dec 2025 09:10:14 -0800 Subject: [PATCH 3/3] Fix test fails on Linux since %ProgramFiles% doesn't expand --- BrowseRouter/Services/ConfigService.cs | 2 +- Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BrowseRouter/Services/ConfigService.cs b/BrowseRouter/Services/ConfigService.cs index f4346df..bf7668b 100644 --- a/BrowseRouter/Services/ConfigService.cs +++ b/BrowseRouter/Services/ConfigService.cs @@ -56,7 +56,7 @@ public IEnumerable GetUrlPreferences(ConfigType configType) _ => [], }; - public virtual async Task> GetFiltersAsync() + public async Task> GetFiltersAsync() { if (string.IsNullOrWhiteSpace(config.FiltersFile)) return []; diff --git a/Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs b/Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs index 6895388..edab928 100644 --- a/Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs +++ b/Tests/BrowseRouter.Tests/Services/BrowserServiceTests.cs @@ -46,7 +46,7 @@ public async Task RedirectsToFirefoxContainer() }, Browsers = new Dictionary { - ["ff-work"] = "\"%ProgramFiles%\\Mozilla Firefox\\firefox.exe\" ext+container:name=Work&url={url}", + ["ff-work"] = "\"Mozilla Firefox\\firefox.exe\" ext+container:name=Work&url={url}", }, }; CatchAllConfig.AddTo(config); @@ -55,7 +55,7 @@ public async Task RedirectsToFirefoxContainer() await new BrowserService(new ConfigService(config), new EmptyNotifyService(), spy) .LaunchAsync("https://www.work.test/foobar", "Fake Window"); - spy.LastPath.Should().Be("C:\\Program Files\\Mozilla Firefox\\firefox.exe"); + spy.LastPath.Should().Be("Mozilla Firefox\\firefox.exe"); spy.LastArgs.Should().Be("ext+container:name=Work&url=https://www.work.test/foobar"); } } \ No newline at end of file