From d596d6b843b5158fd5b3c480dd9460b90d7fbce7 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 24 May 2024 22:24:14 +0800 Subject: [PATCH 1/3] v3.0.5 init commit + Added external domain name detection for PR #168 + Updated uptime error message in 5xx range + Modernized reverse proxy error page template + Added wip permission policy module --- src/main.go | 2 +- src/mod/dynamicproxy/dpcore/dpcore.go | 13 +- src/mod/dynamicproxy/dpcore/utils.go | 32 +++ .../permissionpolicy/permissionpolicy.go | 193 ++++++++++++++++++ .../permissionpolicy/permissionpolicy_test.go | 47 +++++ src/mod/dynamicproxy/templates/hosterror.html | 5 +- src/web/components/uptime.html | 84 ++++++-- src/web/rperror.html | 5 +- 8 files changed, 349 insertions(+), 32 deletions(-) create mode 100644 src/mod/dynamicproxy/permissionpolicy/permissionpolicy.go create mode 100644 src/mod/dynamicproxy/permissionpolicy/permissionpolicy_test.go diff --git a/src/main.go b/src/main.go index c704324..2ca0d9f 100644 --- a/src/main.go +++ b/src/main.go @@ -52,7 +52,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file") var ( name = "Zoraxy" - version = "3.0.4" + version = "3.0.5" nodeUUID = "generic" development = false //Set this to false to use embedded web fs bootTime = time.Now().Unix() diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index ecf6294..d5ffe2c 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -10,6 +10,8 @@ import ( "net/url" "strings" "time" + + "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" ) // ReverseProxy is an HTTP Handler that takes an incoming request and @@ -346,8 +348,11 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr p.Director(outreq) outreq.Close = false - // Always use the original host, see issue #164 - outreq.Host = rrr.OriginalHost + //Only skip origin rewrite iff proxy target require TLS and it is external domain name like github.com + if !(rrr.UseTLS && isExternalDomainName(rrr.ProxyDomain)) { + // Always use the original host, see issue #164 + outreq.Host = rrr.OriginalHost + } // We may modify the header (shallow copied above), so we only copy it. outreq.Header = make(http.Header) @@ -424,6 +429,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr // Copy header from response to client. copyHeader(rw.Header(), res.Header) + // inject permission policy headers + //TODO: Load permission policy from rrr + permissionpolicy.InjectPermissionPolicyHeader(rw, nil) + // The "Trailer" header isn't included in the Transport's response, Build it up from Trailer. if len(res.Trailer) > 0 { trailerKeys := make([]string, 0, len(res.Trailer)) diff --git a/src/mod/dynamicproxy/dpcore/utils.go b/src/mod/dynamicproxy/dpcore/utils.go index 27459b6..03cf20a 100644 --- a/src/mod/dynamicproxy/dpcore/utils.go +++ b/src/mod/dynamicproxy/dpcore/utils.go @@ -1,6 +1,7 @@ package dpcore import ( + "net" "net/url" "strings" ) @@ -60,3 +61,34 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) { return replaceLocationHost(urlString, rrr, useTLS) } + +// isExternalDomainName check and return if the hostname is external domain name (e.g. github.com) +// instead of internal (like 192.168.1.202:8443 (ip address) or domains end with .local or .internal) +func isExternalDomainName(hostname string) bool { + host, _, err := net.SplitHostPort(hostname) + if err != nil { + //hostname doesnt contain port + ip := net.ParseIP(hostname) + if ip != nil { + //IP address, not a domain name + return false + } + } else { + //Hostname contain port, use hostname without port to check if it is ip + ip := net.ParseIP(host) + if ip != nil { + //IP address, not a domain name + return false + } + } + + //Check if it is internal DNS assigned domains + internalDNSTLD := []string{".local", ".internal", ".localhost", ".home.arpa"} + for _, tld := range internalDNSTLD { + if strings.HasSuffix(strings.ToLower(hostname), tld) { + return false + } + } + + return true +} diff --git a/src/mod/dynamicproxy/permissionpolicy/permissionpolicy.go b/src/mod/dynamicproxy/permissionpolicy/permissionpolicy.go new file mode 100644 index 0000000..8f156ed --- /dev/null +++ b/src/mod/dynamicproxy/permissionpolicy/permissionpolicy.go @@ -0,0 +1,193 @@ +package permissionpolicy + +import ( + "fmt" + "net/http" + "strings" +) + +/* + Permisson Policy + + This is a permission policy header modifier that changes + the request permission related policy fields + + author: tobychui +*/ + +type PermissionsPolicy struct { + Accelerometer []string `json:"accelerometer"` + AmbientLightSensor []string `json:"ambient_light_sensor"` + Autoplay []string `json:"autoplay"` + Battery []string `json:"battery"` + Camera []string `json:"camera"` + CrossOriginIsolated []string `json:"cross_origin_isolated"` + DisplayCapture []string `json:"display_capture"` + DocumentDomain []string `json:"document_domain"` + EncryptedMedia []string `json:"encrypted_media"` + ExecutionWhileNotRendered []string `json:"execution_while_not_rendered"` + ExecutionWhileOutOfView []string `json:"execution_while_out_of_viewport"` + Fullscreen []string `json:"fullscreen"` + Geolocation []string `json:"geolocation"` + Gyroscope []string `json:"gyroscope"` + KeyboardMap []string `json:"keyboard_map"` + Magnetometer []string `json:"magnetometer"` + Microphone []string `json:"microphone"` + Midi []string `json:"midi"` + NavigationOverride []string `json:"navigation_override"` + Payment []string `json:"payment"` + PictureInPicture []string `json:"picture_in_picture"` + PublicKeyCredentialsGet []string `json:"publickey_credentials_get"` + ScreenWakeLock []string `json:"screen_wake_lock"` + SyncXHR []string `json:"sync_xhr"` + USB []string `json:"usb"` + WebShare []string `json:"web_share"` + XRSpatialTracking []string `json:"xr_spatial_tracking"` + ClipboardRead []string `json:"clipboard_read"` + ClipboardWrite []string `json:"clipboard_write"` + Gamepad []string `json:"gamepad"` + SpeakerSelection []string `json:"speaker_selection"` + ConversionMeasurement []string `json:"conversion_measurement"` + FocusWithoutUserActivation []string `json:"focus_without_user_activation"` + HID []string `json:"hid"` + IdleDetection []string `json:"idle_detection"` + InterestCohort []string `json:"interest_cohort"` + Serial []string `json:"serial"` + SyncScript []string `json:"sync_script"` + TrustTokenRedemption []string `json:"trust_token_redemption"` + Unload []string `json:"unload"` + WindowPlacement []string `json:"window_placement"` + VerticalScroll []string `json:"vertical_scroll"` +} + +// GetDefaultPermissionPolicy returns a PermissionsPolicy struct with all policies set to * +func GetDefaultPermissionPolicy() *PermissionsPolicy { + return &PermissionsPolicy{ + Accelerometer: []string{"*"}, + AmbientLightSensor: []string{"*"}, + Autoplay: []string{"*"}, + Battery: []string{"*"}, + Camera: []string{"*"}, + CrossOriginIsolated: []string{"*"}, + DisplayCapture: []string{"*"}, + DocumentDomain: []string{"*"}, + EncryptedMedia: []string{"*"}, + ExecutionWhileNotRendered: []string{"*"}, + ExecutionWhileOutOfView: []string{"*"}, + Fullscreen: []string{"*"}, + Geolocation: []string{"*"}, + Gyroscope: []string{"*"}, + KeyboardMap: []string{"*"}, + Magnetometer: []string{"*"}, + Microphone: []string{"*"}, + Midi: []string{"*"}, + NavigationOverride: []string{"*"}, + Payment: []string{"*"}, + PictureInPicture: []string{"*"}, + PublicKeyCredentialsGet: []string{"*"}, + ScreenWakeLock: []string{"*"}, + SyncXHR: []string{"*"}, + USB: []string{"*"}, + WebShare: []string{"*"}, + XRSpatialTracking: []string{"*"}, + ClipboardRead: []string{"*"}, + ClipboardWrite: []string{"*"}, + Gamepad: []string{"*"}, + SpeakerSelection: []string{"*"}, + ConversionMeasurement: []string{"*"}, + FocusWithoutUserActivation: []string{"*"}, + HID: []string{"*"}, + IdleDetection: []string{"*"}, + InterestCohort: []string{"*"}, + Serial: []string{"*"}, + SyncScript: []string{"*"}, + TrustTokenRedemption: []string{"*"}, + Unload: []string{"*"}, + WindowPlacement: []string{"*"}, + VerticalScroll: []string{"*"}, + } +} + +// InjectPermissionPolicyHeader inject the permission policy into headers +func InjectPermissionPolicyHeader(w http.ResponseWriter, policy *PermissionsPolicy) { + //Keep the original Permission Policy if exists, or there are no policy given + if policy == nil || w.Header().Get("Permissions-Policy") != "" { + return + } + + policyHeader := []string{} + + // Helper function to add policy directives + addDirective := func(name string, sources []string) { + if len(sources) > 0 { + if sources[0] == "*" { + //Allow all + policyHeader = append(policyHeader, fmt.Sprintf("%s=%s", name, "*")) + } else { + //Other than "self" which do not need double quote, others domain need double quote in place + formatedSources := []string{} + for _, source := range sources { + if source == "self" { + formatedSources = append(formatedSources, "self") + } else { + formatedSources = append(formatedSources, "\""+source+"\"") + } + } + policyHeader = append(policyHeader, fmt.Sprintf("%s=(%s)", name, strings.Join(formatedSources, " "))) + } + } else { + //There are no setting for this field. Assume no permission + policyHeader = append(policyHeader, fmt.Sprintf("%s=()", name)) + } + } + + // Add each policy directive to the header + addDirective("accelerometer", policy.Accelerometer) + addDirective("ambient-light-sensor", policy.AmbientLightSensor) + addDirective("autoplay", policy.Autoplay) + addDirective("battery", policy.Battery) + addDirective("camera", policy.Camera) + addDirective("cross-origin-isolated", policy.CrossOriginIsolated) + addDirective("display-capture", policy.DisplayCapture) + addDirective("document-domain", policy.DocumentDomain) + addDirective("encrypted-media", policy.EncryptedMedia) + addDirective("execution-while-not-rendered", policy.ExecutionWhileNotRendered) + addDirective("execution-while-out-of-viewport", policy.ExecutionWhileOutOfView) + addDirective("fullscreen", policy.Fullscreen) + addDirective("geolocation", policy.Geolocation) + addDirective("gyroscope", policy.Gyroscope) + addDirective("keyboard-map", policy.KeyboardMap) + addDirective("magnetometer", policy.Magnetometer) + addDirective("microphone", policy.Microphone) + addDirective("midi", policy.Midi) + addDirective("navigation-override", policy.NavigationOverride) + addDirective("payment", policy.Payment) + addDirective("picture-in-picture", policy.PictureInPicture) + addDirective("publickey-credentials-get", policy.PublicKeyCredentialsGet) + addDirective("screen-wake-lock", policy.ScreenWakeLock) + addDirective("sync-xhr", policy.SyncXHR) + addDirective("usb", policy.USB) + addDirective("web-share", policy.WebShare) + addDirective("xr-spatial-tracking", policy.XRSpatialTracking) + addDirective("clipboard-read", policy.ClipboardRead) + addDirective("clipboard-write", policy.ClipboardWrite) + addDirective("gamepad", policy.Gamepad) + addDirective("speaker-selection", policy.SpeakerSelection) + addDirective("conversion-measurement", policy.ConversionMeasurement) + addDirective("focus-without-user-activation", policy.FocusWithoutUserActivation) + addDirective("hid", policy.HID) + addDirective("idle-detection", policy.IdleDetection) + addDirective("interest-cohort", policy.InterestCohort) + addDirective("serial", policy.Serial) + addDirective("sync-script", policy.SyncScript) + addDirective("trust-token-redemption", policy.TrustTokenRedemption) + addDirective("unload", policy.Unload) + addDirective("window-placement", policy.WindowPlacement) + addDirective("vertical-scroll", policy.VerticalScroll) + + // Join the directives and set the header + policyHeaderValue := strings.Join(policyHeader, ", ") + + //Inject the new policy into the header + w.Header().Set("Permissions-Policy", policyHeaderValue) +} diff --git a/src/mod/dynamicproxy/permissionpolicy/permissionpolicy_test.go b/src/mod/dynamicproxy/permissionpolicy/permissionpolicy_test.go new file mode 100644 index 0000000..bca2785 --- /dev/null +++ b/src/mod/dynamicproxy/permissionpolicy/permissionpolicy_test.go @@ -0,0 +1,47 @@ +package permissionpolicy_test + +import ( + "net/http/httptest" + "strings" + "testing" + + "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" +) + +func TestInjectPermissionPolicyHeader(t *testing.T) { + //Prepare the data for permission policy + testPermissionPolicy := permissionpolicy.GetDefaultPermissionPolicy() + testPermissionPolicy.Geolocation = []string{"self"} + testPermissionPolicy.Microphone = []string{"self", "https://example.com"} + testPermissionPolicy.Camera = []string{"*"} + + tests := []struct { + name string + existingHeader string + policy *permissionpolicy.PermissionsPolicy + expectedHeader string + }{ + { + name: "Default policy with a few limitations", + existingHeader: "", + policy: testPermissionPolicy, + expectedHeader: `accelerometer=*, ambient-light-sensor=*, autoplay=*, battery=*, camera=*, cross-origin-isolated=*, display-capture=*, document-domain=*, encrypted-media=*, execution-while-not-rendered=*, execution-while-out-of-viewport=*, fullscreen=*, geolocation=(self), gyroscope=*, keyboard-map=*, magnetometer=*, microphone=(self "https://example.com"), midi=*, navigation-override=*, payment=*, picture-in-picture=*, publickey-credentials-get=*, screen-wake-lock=*, sync-xhr=*, usb=*, web-share=*, xr-spatial-tracking=*, clipboard-read=*, clipboard-write=*, gamepad=*, speaker-selection=*, conversion-measurement=*, focus-without-user-activation=*, hid=*, idle-detection=*, interest-cohort=*, serial=*, sync-script=*, trust-token-redemption=*, unload=*, window-placement=*, vertical-scroll=*`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rr := httptest.NewRecorder() + if tt.existingHeader != "" { + rr.Header().Set("Permissions-Policy", tt.existingHeader) + } + + permissionpolicy.InjectPermissionPolicyHeader(rr, tt.policy) + + gotHeader := rr.Header().Get("Permissions-Policy") + if !strings.Contains(gotHeader, tt.expectedHeader) { + t.Errorf("got header %s, want %s", gotHeader, tt.expectedHeader) + } + }) + } +} diff --git a/src/mod/dynamicproxy/templates/hosterror.html b/src/mod/dynamicproxy/templates/hosterror.html index aef97d3..0325ac0 100644 --- a/src/mod/dynamicproxy/templates/hosterror.html +++ b/src/mod/dynamicproxy/templates/hosterror.html @@ -14,7 +14,7 @@ 404 - Host Not Found