mirror of
https://github.com/fosrl/badger.git
synced 2026-02-08 05:56:46 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0a2d76844 | ||
|
|
be5015cc2a | ||
|
|
f00a92c7af | ||
|
|
1d15b5b175 | ||
|
|
3eea242a8e | ||
|
|
2bc45f31f7 | ||
|
|
ad3c3b71e6 | ||
|
|
52f47fa24e | ||
|
|
83e894f23f | ||
|
|
7d75628d86 | ||
|
|
4adf71ec3c | ||
|
|
881b9d665e | ||
|
|
cc45f414de | ||
|
|
14df42f0ab | ||
|
|
88c453cae1 | ||
|
|
cab2424e6e | ||
|
|
d3adb46c6a | ||
|
|
a8ba33b9b5 | ||
|
|
11c7340e63 | ||
|
|
e933d3ca62 | ||
|
|
d553b6ca77 | ||
|
|
0baba997d7 |
BIN
.assets/icon.png
Normal file
BIN
.assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
47
.github/DISCUSSION_TEMPLATE/feature-requests.yml
vendored
Normal file
47
.github/DISCUSSION_TEMPLATE/feature-requests.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: A clear and concise summary of the requested feature.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
Why is this feature important?
|
||||
Explain the problem this feature would solve or what use case it would enable.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: |
|
||||
How would you like to see this feature implemented?
|
||||
Provide as much detail as possible about the desired behavior, configuration, or changes.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Describe any alternative solutions or workarounds you've thought about.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, mockups, or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before submitting, please:
|
||||
- Check if there is an existing issue for this feature.
|
||||
- Clearly explain the benefit and use case.
|
||||
- Be as specific as possible to help contributors evaluate and implement.
|
||||
51
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
Normal file
51
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report
|
||||
labels: []
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Please fill out the relevant details below for your environment.
|
||||
value: |
|
||||
- OS Type & Version: (e.g., Ubuntu 22.04)
|
||||
- Pangolin Version:
|
||||
- Gerbil Version:
|
||||
- Traefik Version:
|
||||
- Newt Version:
|
||||
- Olm Version: (if applicable)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: |
|
||||
Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below.
|
||||
|
||||
If using code blocks, make sure syntax highlighting is correct and double-check that the rendered preview is not broken.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Contributors should be able to follow the steps provided in order to reproduce the bug.
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Need help or have questions?
|
||||
url: https://github.com/orgs/fosrl/discussions
|
||||
about: Ask questions, get help, and discuss with other community members
|
||||
- name: Request a Feature
|
||||
url: https://github.com/orgs/fosrl/discussions/new?category=feature-requests
|
||||
about: Feature requests should be opened as discussions so others can upvote and comment
|
||||
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
## Community Contribution License Agreement
|
||||
By creating this pull request, I grant the project maintainers an unlimited,
|
||||
perpetual license to use, modify, and redistribute these contributions under any terms they
|
||||
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
|
||||
represent that I have the right to grant this license for all contributed content.
|
||||
|
||||
## Description
|
||||
|
||||
|
||||
## How to test?
|
||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
go.sum
|
||||
.DS_Store
|
||||
@@ -1 +1 @@
|
||||
1.23.2
|
||||
1.25
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
displayName: Fossorial Badger
|
||||
displayName: Pangolin (Badger)
|
||||
type: middleware
|
||||
iconPath: .assets/icon.png
|
||||
|
||||
import: github.com/fosrl/badger
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Fossorial LLC
|
||||
Copyright (c) 2025 Fossorial Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
86
README.md
86
README.md
@@ -1,18 +1,23 @@
|
||||
# Badger Plugin for Traefik with Pangolin Integration
|
||||
# Pangolin Middleware: Badger
|
||||
|
||||
Badger is a middleware plugin designed to work with the Traefik reverse proxy in conjunction with [Pangolin](https://github.com/fosrl/pangolin), a multi-tenant tunneled reverse proxy server and management interface with identity and access management. Badger acts as an authentication bouncer, ensuring only authenticated and authorized requests are allowed through the proxy.
|
||||
Badger is a middleware plugin designed to work with Traefik in conjunction with [Pangolin](https://github.com/fosrl/pangolin), an identity-aware reverse proxy and zero-trust VPN. Badger acts as an authentication bouncer, ensuring only authenticated and authorized requests are allowed through the proxy.
|
||||
|
||||
This plugin is **required** to be configured alongside [Pangolin](https://github.com/fosrl/pangolin) to enforce secure authentication and session management.
|
||||
> [!NOTE]
|
||||
> Badger can also be used standalone for IP handling (Cloudflare and custom proxy support) without Pangolin. Simply set `disableForwardAuth: true` in your configuration. See the [Disabling Forward Auth](#disabling-forward-auth) section below for details.
|
||||
|
||||
This plugin is **required** to be installed alongside [Pangolin](https://github.com/fosrl/pangolin) to enforce secure authentication and session management.
|
||||
|
||||
## Installation
|
||||
|
||||
Learn how to set up [Pangolin](https://github.com/fosrl/pangolin) and Badger in the [Pangolin Documentation](https://github.com/fosrl/pangolin).
|
||||
Badger is automatically installed with Pangolin. Learn how to install Pangolin in the [Pangolin Documentation](https://docs.pangolin.net/self-host/quick-install).
|
||||
|
||||
## Configuration
|
||||
|
||||
Badger requires the following configuration parameters to be specified in your [Traefik configuration file](https://doc.traefik.io/traefik/getting-started/configuration-overview/). These coincide with the separate [Pangolin](https://github.com/fosrl/pangolin) configuration file.
|
||||
Pangolin will provide the necessary configuration to Badger automatically via the HTTP provider. However, you can override the configuration settings by manually providing them in the Traefik config.
|
||||
|
||||
### Configuration Options
|
||||
### Required Configuration Options
|
||||
|
||||
When forward auth is enabled (default), the following options are required:
|
||||
|
||||
```yaml
|
||||
apiBaseUrl: "http://localhost:3001/api/v1"
|
||||
@@ -20,6 +25,75 @@ userSessionCookieName: "p_session_token"
|
||||
resourceSessionRequestParam: "p_session_request"
|
||||
```
|
||||
|
||||
### Disabling Forward Auth
|
||||
|
||||
To disable forward auth and only use IP handling, set `disableForwardAuth: true`. When enabled, all requests pass through without authentication, and the required configuration options above are not needed:
|
||||
|
||||
Only do this if you do not need Pangolin's authentication features and only want IP handling.
|
||||
|
||||
```yaml
|
||||
disableForwardAuth: true
|
||||
```
|
||||
|
||||
### IP Handling Configuration
|
||||
|
||||
Badger automatically extracts the real client IP from requests. By default, it trusts Cloudflare IP ranges and uses the `CF-Connecting-IP` header.
|
||||
|
||||
#### Using with Cloudflare (Default)
|
||||
|
||||
No additional configuration needed. Badger automatically:
|
||||
|
||||
- Trusts Cloudflare IP ranges
|
||||
- Extracts IP from `CF-Connecting-IP` header
|
||||
- Sets `X-Real-IP` and `X-Forwarded-For` headers for downstream services
|
||||
|
||||
#### Using without Cloudflare
|
||||
|
||||
If you're using a different proxy or load balancer, configure custom trusted IPs and/or a custom IP header:
|
||||
|
||||
Ensure you always disable the default Cloudflare IP ranges by setting `disableDefaultCFIPs: true` and provide your own trusted IP ranges in CIDR format under `trustip` if using a different proxy.
|
||||
|
||||
```yaml
|
||||
apiBaseUrl: "http://localhost:3001/api/v1"
|
||||
userSessionCookieName: "p_session_token"
|
||||
resourceSessionRequestParam: "p_session_request"
|
||||
|
||||
# Disable Cloudflare IP ranges
|
||||
disableDefaultCFIPs: true
|
||||
|
||||
# Add your proxy/load balancer IP ranges (CIDR format)
|
||||
trustip:
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
|
||||
# Optional: Use a custom header instead of CF-Connecting-IP
|
||||
customIPHeader: "X-Forwarded-For"
|
||||
```
|
||||
|
||||
### Configuration Options Reference
|
||||
|
||||
| Option | Type | Required\* | Default | Description |
|
||||
| ----------------------------- | -------- | ---------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| `disableForwardAuth` | bool | No | `false` | Disable forward auth; only IP handling is performed |
|
||||
| `apiBaseUrl` | string | Yes\* | - | Base URL of the Pangolin API |
|
||||
| `userSessionCookieName` | string | Yes\* | - | Cookie name for user sessions |
|
||||
| `resourceSessionRequestParam` | string | Yes\* | - | Query parameter name for resource session requests |
|
||||
| `trustip` | []string | No | `[]` | Array of trusted IP ranges in CIDR format |
|
||||
| `disableDefaultCFIPs` | bool | No | `false` | Disable default Cloudflare IP ranges |
|
||||
| `customIPHeader` | string | No | `""` | Custom header name to extract IP from (only used if request is from trusted source) |
|
||||
|
||||
\* Required only when `disableForwardAuth` is `false` (default)
|
||||
|
||||
## Updating Cloudflare IPs
|
||||
|
||||
To update the Cloudflare IP ranges, run:
|
||||
|
||||
```bash
|
||||
./updateCFIps.sh
|
||||
```
|
||||
|
||||
This fetches the latest IP ranges from Cloudflare and updates `ips/ips.go`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
28
ips/ips.go
Normal file
28
ips/ips.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package ips
|
||||
|
||||
func CFIPs() []string {
|
||||
return []string{
|
||||
"173.245.48.0/20",
|
||||
"103.21.244.0/22",
|
||||
"103.22.200.0/22",
|
||||
"103.31.4.0/22",
|
||||
"141.101.64.0/18",
|
||||
"108.162.192.0/18",
|
||||
"190.93.240.0/20",
|
||||
"188.114.96.0/20",
|
||||
"197.234.240.0/22",
|
||||
"198.41.128.0/17",
|
||||
"162.158.0.0/15",
|
||||
"104.16.0.0/13",
|
||||
"104.24.0.0/14",
|
||||
"172.64.0.0/13",
|
||||
"131.0.72.0/22",
|
||||
"2400:cb00::/32",
|
||||
"2606:4700::/32",
|
||||
"2803:f800::/32",
|
||||
"2405:b500::/32",
|
||||
"2405:8100::/32",
|
||||
"2a06:98c0::/29",
|
||||
"2c0f:f248::/32",
|
||||
}
|
||||
}
|
||||
242
main.go
242
main.go
@@ -5,22 +5,41 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fosrl/badger/ips"
|
||||
"github.com/fosrl/badger/version"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
APIBaseUrl string `json:"apiBaseUrl"`
|
||||
UserSessionCookieName string `json:"userSessionCookieName"`
|
||||
ResourceSessionRequestParam string `json:"resourceSessionRequestParam"`
|
||||
APIBaseUrl string `json:"apiBaseUrl,omitempty"`
|
||||
UserSessionCookieName string `json:"userSessionCookieName,omitempty"`
|
||||
ResourceSessionRequestParam string `json:"resourceSessionRequestParam,omitempty"`
|
||||
DisableForwardAuth bool `json:"disableForwardAuth,omitempty"`
|
||||
TrustIP []string `json:"trustip,omitempty"`
|
||||
DisableDefaultCFIPs bool `json:"disableDefaultCFIPs,omitempty"`
|
||||
CustomIPHeader string `json:"customIPHeader,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
xRealIP = "X-Real-Ip"
|
||||
xForwardFor = "X-Forwarded-For"
|
||||
xForwardProto = "X-Forwarded-Proto"
|
||||
cfConnectingIP = "CF-Connecting-IP"
|
||||
cfVisitor = "CF-Visitor"
|
||||
)
|
||||
|
||||
type Badger struct {
|
||||
next http.Handler
|
||||
name string
|
||||
apiBaseUrl string
|
||||
userSessionCookieName string
|
||||
resourceSessionRequestParam string
|
||||
disableForwardAuth bool
|
||||
trustIP []*net.IPNet
|
||||
customIPHeader string
|
||||
}
|
||||
|
||||
type VerifyBody struct {
|
||||
@@ -34,13 +53,20 @@ type VerifyBody struct {
|
||||
RequestIP *string `json:"requestIp,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Query map[string]string `json:"query,omitempty"`
|
||||
BadgerVersion string `json:"badgerVersion,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyResponse struct {
|
||||
Data struct {
|
||||
Valid bool `json:"valid"`
|
||||
RedirectURL *string `json:"redirectUrl"`
|
||||
ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
|
||||
HeaderAuthChallenged bool `json:"headerAuthChallenged"`
|
||||
Valid bool `json:"valid"`
|
||||
RedirectURL *string `json:"redirectUrl"`
|
||||
Username *string `json:"username,omitempty"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Role *string `json:"role,omitempty"`
|
||||
ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
|
||||
PangolinVersion *string `json:"pangolinVersion,omitempty"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
@@ -63,16 +89,61 @@ func CreateConfig() *Config {
|
||||
}
|
||||
|
||||
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
|
||||
return &Badger{
|
||||
badger := &Badger{
|
||||
next: next,
|
||||
name: name,
|
||||
apiBaseUrl: config.APIBaseUrl,
|
||||
userSessionCookieName: config.UserSessionCookieName,
|
||||
resourceSessionRequestParam: config.ResourceSessionRequestParam,
|
||||
}, nil
|
||||
disableForwardAuth: config.DisableForwardAuth,
|
||||
customIPHeader: config.CustomIPHeader,
|
||||
}
|
||||
|
||||
// Validate required fields only if forward auth is enabled
|
||||
if !config.DisableForwardAuth {
|
||||
if config.APIBaseUrl == "" {
|
||||
return nil, fmt.Errorf("apiBaseUrl is required when forward auth is enabled")
|
||||
}
|
||||
if config.UserSessionCookieName == "" {
|
||||
return nil, fmt.Errorf("userSessionCookieName is required when forward auth is enabled")
|
||||
}
|
||||
if config.ResourceSessionRequestParam == "" {
|
||||
return nil, fmt.Errorf("resourceSessionRequestParam is required when forward auth is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
if config.TrustIP != nil {
|
||||
for _, v := range config.TrustIP {
|
||||
_, trustip, err := net.ParseCIDR(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
badger.trustIP = append(badger.trustIP, trustip)
|
||||
}
|
||||
}
|
||||
|
||||
if !config.DisableDefaultCFIPs {
|
||||
for _, v := range ips.CFIPs() {
|
||||
_, trustip, err := net.ParseCIDR(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
badger.trustIP = append(badger.trustIP, trustip)
|
||||
}
|
||||
}
|
||||
|
||||
return badger, nil
|
||||
}
|
||||
|
||||
func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
realIP := p.getRealIP(req)
|
||||
p.setIPHeaders(req, realIP)
|
||||
|
||||
if p.disableForwardAuth {
|
||||
p.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
cookies := p.extractCookies(req)
|
||||
|
||||
queryValues := req.URL.Query()
|
||||
@@ -81,7 +152,7 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
body := ExchangeSessionBody{
|
||||
RequestToken: &sessionRequestValue,
|
||||
RequestHost: &req.Host,
|
||||
RequestIP: &req.RemoteAddr,
|
||||
RequestIP: &realIP,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(body)
|
||||
@@ -157,9 +228,10 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
RequestPath: &req.URL.Path,
|
||||
RequestMethod: &req.Method,
|
||||
TLS: req.TLS != nil,
|
||||
RequestIP: &req.RemoteAddr,
|
||||
RequestIP: &realIP,
|
||||
Headers: headers,
|
||||
Query: queryParams,
|
||||
BadgerVersion: version.Version,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(cookieData)
|
||||
@@ -191,12 +263,31 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Del("Remote-User")
|
||||
req.Header.Del("Remote-Email")
|
||||
req.Header.Del("Remote-Name")
|
||||
req.Header.Del("Remote-Role")
|
||||
|
||||
if result.Data.ResponseHeaders != nil {
|
||||
for key, value := range result.Data.ResponseHeaders {
|
||||
rw.Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if result.Data.HeaderAuthChallenged {
|
||||
fmt.Println("Badger: challenging client for header authentication")
|
||||
rw.Header().Add("WWW-Authenticate", "Basic realm=\"pangolin\"")
|
||||
|
||||
if result.Data.RedirectURL != nil && *result.Data.RedirectURL != "" {
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
rw.Write([]byte(p.renderRedirectPage(*result.Data.RedirectURL)))
|
||||
} else {
|
||||
http.Error(rw, "Unauthorized", http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result.Data.RedirectURL != nil && *result.Data.RedirectURL != "" {
|
||||
fmt.Println("Badger: Redirecting to", *result.Data.RedirectURL)
|
||||
http.Redirect(rw, req, *result.Data.RedirectURL, http.StatusFound)
|
||||
@@ -204,6 +295,23 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if result.Data.Valid {
|
||||
|
||||
if result.Data.Username != nil {
|
||||
req.Header.Add("Remote-User", *result.Data.Username)
|
||||
}
|
||||
|
||||
if result.Data.Email != nil {
|
||||
req.Header.Add("Remote-Email", *result.Data.Email)
|
||||
}
|
||||
|
||||
if result.Data.Name != nil {
|
||||
req.Header.Add("Remote-Name", *result.Data.Name)
|
||||
}
|
||||
|
||||
if result.Data.Role != nil {
|
||||
req.Header.Add("Remote-Role", *result.Data.Role)
|
||||
}
|
||||
|
||||
fmt.Println("Badger: Valid session")
|
||||
p.next.ServeHTTP(rw, req)
|
||||
return
|
||||
@@ -234,3 +342,117 @@ func (p *Badger) getScheme(req *http.Request) string {
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (p *Badger) renderRedirectPage(redirectURL string) string {
|
||||
return fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting...</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<p>Redirecting...</p>
|
||||
<p>If you are not redirected automatically, <a href="%s">click here</a>.</p>
|
||||
</div>
|
||||
<script>
|
||||
window.location.href = "%s";
|
||||
</script>
|
||||
</body>
|
||||
</html>`, redirectURL, redirectURL)
|
||||
}
|
||||
|
||||
func (p *Badger) getRealIP(req *http.Request) string {
|
||||
// Check if request comes from a trusted source
|
||||
isTrusted := p.isTrustedIP(req.RemoteAddr)
|
||||
|
||||
// If custom IP header is configured, use it
|
||||
if p.customIPHeader != "" {
|
||||
if customIP := req.Header.Get(p.customIPHeader); customIP != "" && isTrusted {
|
||||
return customIP
|
||||
}
|
||||
}
|
||||
|
||||
// Default: use CF-Connecting-IP if from trusted source
|
||||
if isTrusted {
|
||||
if cfIP := req.Header.Get(cfConnectingIP); cfIP != "" {
|
||||
return cfIP
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: extract IP from RemoteAddr
|
||||
ip, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
// If parsing fails, return RemoteAddr as-is (might be just IP without port)
|
||||
return req.RemoteAddr
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func (p *Badger) isTrustedIP(remoteAddr string) bool {
|
||||
ipStr, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
for _, network := range p.trustIP {
|
||||
if network.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Badger) setIPHeaders(req *http.Request, realIP string) {
|
||||
isTrusted := p.isTrustedIP(req.RemoteAddr)
|
||||
|
||||
if isTrusted {
|
||||
// Handle CF-Visitor header for scheme
|
||||
if req.Header.Get(cfVisitor) != "" {
|
||||
var cfVisitorValue struct {
|
||||
Scheme string `json:"scheme"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(req.Header.Get(cfVisitor)), &cfVisitorValue); err == nil {
|
||||
req.Header.Set(xForwardProto, cfVisitorValue.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
// Set headers with the real IP (already extracted from CF-Connecting-IP or custom header)
|
||||
req.Header.Set(xForwardFor, realIP)
|
||||
req.Header.Set(xRealIP, realIP)
|
||||
} else {
|
||||
// Not from trusted source, use direct IP
|
||||
req.Header.Set(xRealIP, realIP)
|
||||
// Remove CF headers if present
|
||||
req.Header.Del(cfVisitor)
|
||||
req.Header.Del(cfConnectingIP)
|
||||
}
|
||||
}
|
||||
|
||||
56
updateCFIps.sh
Executable file
56
updateCFIps.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
rm CFIPs.txt
|
||||
curl https://www.cloudflare.com/ips-v4 >>CFIPs.txt
|
||||
echo "" >>CFIPs.txt
|
||||
curl https://www.cloudflare.com/ips-v6 >>CFIPs.txt
|
||||
echo "" >>CFIPs.txt
|
||||
|
||||
OUTPUT_GO_CONFIG="./ips/ips.go"
|
||||
OUTPUT_GO_CONFIG_OLD="./ips-temp.go"
|
||||
|
||||
mv $OUTPUT_GO_CONFIG $OUTPUT_GO_CONFIG_OLD
|
||||
|
||||
echo "// Package ips contains a list of current cloud flare IP ranges" >>$OUTPUT_GO_CONFIG
|
||||
echo "package ips" >>$OUTPUT_GO_CONFIG
|
||||
echo "" >>$OUTPUT_GO_CONFIG
|
||||
echo "// CFIPs is the CloudFlare Server IP list (this is checked on build)." >>$OUTPUT_GO_CONFIG
|
||||
echo "func CFIPs() []string {" >>$OUTPUT_GO_CONFIG
|
||||
echo " return []string{" >>$OUTPUT_GO_CONFIG
|
||||
|
||||
cat CFIPs.txt | while read line || [[ -n $line ]]; do
|
||||
printf '%s\n' "CF IP: $line"
|
||||
echo " \"${line}\"," >>$OUTPUT_GO_CONFIG
|
||||
done
|
||||
|
||||
echo " }" >>$OUTPUT_GO_CONFIG
|
||||
echo "}" >>$OUTPUT_GO_CONFIG
|
||||
|
||||
rm CFIPs.txt
|
||||
|
||||
if [ "${1}" == "pc" ]; then
|
||||
echo "Run on pre-commit hook."
|
||||
if cmp --silent -- "$OUTPUT_GO_CONFIG" "$OUTPUT_GO_CONFIG_OLD"; then
|
||||
echo "No changes, nothing to worry about"
|
||||
else
|
||||
echo "Cloud flare have changed their IPs, adding changes to commit."
|
||||
touch ./.commit
|
||||
fi
|
||||
|
||||
rm $OUTPUT_GO_CONFIG_OLD
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ "${1}" != "ci" ]; then
|
||||
echo "Not run on CI, exit ok"
|
||||
rm $OUTPUT_GO_CONFIG_OLD
|
||||
exit
|
||||
fi
|
||||
|
||||
if cmp --silent -- "$OUTPUT_GO_CONFIG" "$OUTPUT_GO_CONFIG_OLD"; then
|
||||
echo "No changes to Cloud Flare IP list"
|
||||
rm $OUTPUT_GO_CONFIG_OLD
|
||||
else
|
||||
echo "Cloud flare have changed their IPs, re-run updateCFIps.sh and commit the changes!"
|
||||
rm $OUTPUT_GO_CONFIG_OLD
|
||||
exit 6
|
||||
fi
|
||||
|
||||
3
version/version.go
Normal file
3
version/version.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package version
|
||||
|
||||
const Version = "1.3.1"
|
||||
Reference in New Issue
Block a user