Skip to content

Commit

Permalink
Merge pull request #174 from tobychui/v3.0.5
Browse files Browse the repository at this point in the history
- Optimized uptime monitor error message
- Optimized detection logic for internal proxy target and header rewrite condition for HTTP_HOST
- Fixed ovh DNS challenge provider form generator bug
- Configuration for OVH DNS Challenge
- Added permission policy module (not enabled)
- Added single-use cookiejar to uptime monitor request client to handle cookie issues on some poorly written back-end server
  • Loading branch information
tobychui authored May 26, 2024
2 parents e73f9b4 + 7a3db09 commit ce8741b
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 65 deletions.
2 changes: 1 addition & 1 deletion src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
13 changes: 11 additions & 2 deletions src/mod/dynamicproxy/dpcore/dpcore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
32 changes: 32 additions & 0 deletions src/mod/dynamicproxy/dpcore/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dpcore

import (
"net"
"net/url"
"strings"
)
Expand Down Expand Up @@ -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
}
193 changes: 193 additions & 0 deletions src/mod/dynamicproxy/permissionpolicy/permissionpolicy.go
Original file line number Diff line number Diff line change
@@ -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)
}
47 changes: 47 additions & 0 deletions src/mod/dynamicproxy/permissionpolicy/permissionpolicy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
5 changes: 1 addition & 4 deletions src/mod/dynamicproxy/templates/hosterror.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,14 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
<title>404 - Host Not Found</title>
<style>
h1, h2, h3, h4, h5, p, a, span{
h1, h2, h3, h4, h5, p, a, span, .ui.list .item{
font-family: 'Noto Sans TC', sans-serif;
font-weight: 300;
color: rgb(88, 88, 88)
}

.diagram{
background-color: #ebebeb;
box-shadow:
inset 0px 11px 8px -10px #CCC,
inset 0px -11px 8px -10px #CCC;
padding-bottom: 2em;
}

Expand Down
24 changes: 22 additions & 2 deletions src/mod/uptime/uptime.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"encoding/json"
"log"
"net/http"
"net/http/cookiejar"
"strings"
"time"

"golang.org/x/net/publicsuffix"
"imuslab.com/zoraxy/mod/utils"
)

Expand Down Expand Up @@ -217,11 +219,24 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
}

func getWebsiteStatus(url string) (int, error) {
// Create a one-time use cookie jar to store cookies
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
log.Fatal(err)
}

client := http.Client{
Jar: jar,
Timeout: 10 * time.Second,
}

resp, err := client.Get(url)
req, _ := http.NewRequest("GET", url, nil)
req.Header = http.Header{
"User-Agent": {"zoraxy-uptime/1.1"},
}

resp, err := client.Do(req)
//resp, err := client.Get(url)
if err != nil {
//Try replace the http with https and vise versa
rewriteURL := ""
Expand All @@ -231,7 +246,12 @@ func getWebsiteStatus(url string) (int, error) {
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
}

resp, err = client.Get(rewriteURL)
req, _ := http.NewRequest("GET", rewriteURL, nil)
req.Header = http.Header{
"User-Agent": {"zoraxy-uptime/1.1"},
}

resp, err := client.Do(req)
if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online
Expand Down
Loading

0 comments on commit ce8741b

Please sign in to comment.