Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
pkg
bin
.hg
*.sublime-project
*.sublime-workspace
*.log
Expand All @@ -12,3 +11,5 @@ router/router-error-test.log
.idea
/router.exe
router
**/*.exe
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you move the test files one level deeper then the exe that gets created on linux will be cors. So please add this to help people on linux that accidentally ran go build in the folder

Suggested change
**/*.exe
**/*.exe
**/cors

**/cors
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ We needed a performant and well-behaved front door to all of our services which
could forward HTTP as well as Websockets. Nginx fits this bill, but since we
need to run on Windows, Nginx is a non-starter. We tried for some time to get
Apache to do this job, but we failed to get Apache to robustly forward websockets.

Testing
-------
For instructions on testing CORS settings using the included utilities,
see [test/cors/README.md](test/cors/README.md).
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ require (
github.com/IMQS/serviceconfigsgo v1.4.0
golang.org/x/net v0.29.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gotest.tools/v3 v3.5.1
)

require (
github.com/google/go-cmp v0.5.9 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
1 change: 1 addition & 0 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ type ConfigHTTP struct {
ResponseHeaderTimeout int
RedirectHTTP bool
AutomaticGzip automaticGzip
Origins map[string]struct{}
Comment thread
FritzOnFire marked this conversation as resolved.
}

type ConfigRoute struct {
Expand Down
42 changes: 42 additions & 0 deletions server/logic_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package server

import (
"gotest.tools/v3/assert"
"net/http"
"net/url"
"testing"
Expand Down Expand Up @@ -148,3 +149,44 @@ func TestInvalidRoutes(t *testing.T) {
"/albjs/extile/(.*)": 123
}}`, "Match /albjs/extile/(.*) has invalid value type. Must be either a string, or an object with 'Target' and 'ValidHosts'")
}

func TestOrigins(t *testing.T) {
jsonString := `{
"HTTP": {
"Origins": {}
}
}`
routerConf := &Config{}
err := routerConf.LoadString(jsonString)
assert.NilError(t, err)
assert.Equal(t, len(routerConf.HTTP.Origins), 0)
Comment thread
FritzOnFire marked this conversation as resolved.
_, ok := routerConf.HTTP.Origins["https://example.com"]
Comment thread
FritzOnFire marked this conversation as resolved.
assert.Equal(t, ok, false)

jsonString = `{
"HTTP": {
"Origins": {
"https://example.com" : {}
}
}
}`
routerConf = &Config{}
err = routerConf.LoadString(jsonString)
assert.NilError(t, err)
assert.Equal(t, len(routerConf.HTTP.Origins), 1)
_, ok = routerConf.HTTP.Origins["https://example.com"]
assert.Equal(t, ok, true)
_, ok = routerConf.HTTP.Origins["https://example-bad.com"]
assert.Equal(t, ok, false)
Comment thread
FritzOnFire marked this conversation as resolved.

// null test
jsonString = `{
"HTTP": {}
}`
routerConf = &Config{}
err = routerConf.LoadString(jsonString)
assert.NilError(t, err)
assert.Equal(t, len(routerConf.HTTP.Origins), 0)
_, ok = routerConf.HTTP.Origins["https://example.com"]
assert.Equal(t, ok, false)
Comment thread
FritzOnFire marked this conversation as resolved.
}
16 changes: 16 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,22 @@ the response was sent. This would then result in s.httpTransport.RoundTrip(clean
an EOF error when it tried to re-use that TCP connection.
*/
func (s *Server) forwardHttp(w http.ResponseWriter, req *http.Request, newurl string) {
// Set the Access-Control-Allow-Origin header, based on allow-list
s.errorLog.Infof("Forwarding from \"%v\"", req.Header.Get("Origin"))
_, ok := s.configHttp.Origins[req.Header.Get("Origin")]
if ok {
w.Header().Set("Access-Control-Allow-Origin", req.Header.Get("Origin"))
w.Header().Set("Access-Control-Allow-Credentials", "true")

// Handle preflight OPTIONS requests
if req.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.WriteHeader(http.StatusOK)
return
}
}
Comment on lines +460 to +474
Copy link
Copy Markdown
Contributor

@FritzOnFire FritzOnFire Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to minimize the impact on our standard config.

Suggested change
// Set the Access-Control-Allow-Origin header, based on allow-list
s.errorLog.Infof("Forwarding from \"%v\"", req.Header.Get("Origin"))
_, ok := s.configHttp.Origins[req.Header.Get("Origin")]
if ok {
w.Header().Set("Access-Control-Allow-Origin", req.Header.Get("Origin"))
w.Header().Set("Access-Control-Allow-Credentials", "true")
// Handle preflight OPTIONS requests
if req.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.WriteHeader(http.StatusOK)
return
}
}
if len(s.configHttp.Origins) > 0 {
// Set the Access-Control-Allow-Origin header, based on allow-list
og := req.Header.Get("Origin")
s.errorLog.Infof("Forwarding from \"%v\"", og)
if _, ok := s.configHttp.Origins[og]; ok {
w.Header().Set("Access-Control-Allow-Origin", og)
w.Header().Set("Access-Control-Allow-Credentials", "true")
// Handle preflight OPTIONS requests
if req.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.WriteHeader(http.StatusOK)
return
}
}
}


cleaned, err := http.NewRequest(req.Method, newurl, req.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand Down
25 changes: 25 additions & 0 deletions test/cors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Test Folder

This folder contains utilities for testing CORS (Cross-Origin Resource Sharing) settings on a remote server.

## Files

### servefile.go

A minimal Go HTTP server that serves the `test.html` file on
`http://localhost:8080`. This allows you to easily load the test page in your
browser.

### test.html

A simple HTML page with JavaScript that lets you specify a remote server URL and
make a cross-origin request to it. This helps you verify if your target server's
CORS settings are correct by observing the response and any errors.
Make sure your of your working directory to avoid path errors.

## Usage
1. Run `servefile.go` with `go run servefile.go`.
2. Open `http://localhost:8080` in your browser.
3. Enter the remote server URL you want to test and click "Test CORS".
4. Observe the results and adjust your target server's CORS configuration as needed.

20 changes: 20 additions & 0 deletions test/cors/servefile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import (
"fmt"
"net/http"
)

type TS struct {
}

func (TS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "test.html")
}

func main() {
fmt.Println("Starting static file server...")
testServe := TS{}
fmt.Println("CORS test web page running on http://localhost:8080")
http.ListenAndServe(":8080", testServe)
}
38 changes: 38 additions & 0 deletions test/cors/test.html
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like fluff, but please move this file one level deeper in case we need to use this folder for other more involved testing setups

New Dir: test/cors.

But then we can probably rename the file to test.html, as it would be obvious what the file is used for.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CORS Test Utility</title>
<style>
body { font-family: Arial, sans-serif; margin: 2em; }
input, button { font-size: 1em; margin: 0.5em 0; }
#result { margin-top: 1em; white-space: pre-wrap; }
</style>
</head>
<body>
<h2>CORS Test Utility</h2>
<label for="url">Remote Server URL:</label>
<input type="text" id="url" size="50" placeholder="https://your-server.com/api/test">
<button onclick="testCORS()">Test CORS</button>
<div id="result"></div>
<script>
function testCORS() {
const url = document.getElementById('url').value;
const resultDiv = document.getElementById('result');
resultDiv.textContent = 'Testing...';
fetch(url, { method: 'GET', mode: 'cors' })
.then(response => {
resultDiv.textContent = `Status: ${response.status}\nCORS appears to be working.`;
return response.text();
})
.then(text => {
resultDiv.textContent += `\nResponse Body:\n${text}`;
})
.catch(error => {
resultDiv.textContent = `Error: ${error}\nLikely a CORS issue or network error.`;
});
}
</script>
</body>
</html>

Loading