mirror of
https://github.com/Gouryella/drip.git
synced 2026-02-23 21:00:44 +00:00
feat(cli): add proxy authentication support
Add the --auth parameter to enable proxy authentication for HTTP and HTTPS tunnels, supporting password verification and session management. - Add --auth flag in CLI to set proxy authentication password - Implement server-side authentication handling and login page - Support Cookie-based session management and validation - Add protocol message definitions related to authentication
This commit is contained in:
@@ -3,6 +3,9 @@ package proxy
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -32,6 +35,57 @@ var bufioReaderPool = sync.Pool{
|
||||
}
|
||||
|
||||
const openStreamTimeout = 3 * time.Second
|
||||
const authCookieName = "drip_auth"
|
||||
const authSessionDuration = 24 * time.Hour
|
||||
|
||||
type authSession struct {
|
||||
subdomain string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type authSessionStore struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*authSession
|
||||
}
|
||||
|
||||
var sessionStore = &authSessionStore{
|
||||
sessions: make(map[string]*authSession),
|
||||
}
|
||||
|
||||
func (s *authSessionStore) create(subdomain string) string {
|
||||
token := generateSessionToken()
|
||||
s.mu.Lock()
|
||||
s.sessions[token] = &authSession{
|
||||
subdomain: subdomain,
|
||||
expiresAt: time.Now().Add(authSessionDuration),
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return token
|
||||
}
|
||||
|
||||
func (s *authSessionStore) validate(token, subdomain string) bool {
|
||||
s.mu.RLock()
|
||||
session, ok := s.sessions[token]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if time.Now().After(session.expiresAt) {
|
||||
s.mu.Lock()
|
||||
delete(s.sessions, token)
|
||||
s.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
return session.subdomain == subdomain
|
||||
}
|
||||
|
||||
func generateSessionToken() string {
|
||||
b := make([]byte, 32)
|
||||
rand.Read(b)
|
||||
hash := sha256.Sum256(b)
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
manager *tunnel.Manager
|
||||
@@ -45,13 +99,13 @@ var privateNetworks []*net.IPNet
|
||||
|
||||
func init() {
|
||||
privateCIDRs := []string{
|
||||
"127.0.0.0/8", // IPv4 loopback
|
||||
"10.0.0.0/8", // RFC 1918 Class A
|
||||
"172.16.0.0/12", // RFC 1918 Class B
|
||||
"192.168.0.0/16", // RFC 1918 Class C
|
||||
"::1/128", // IPv6 loopback
|
||||
"fc00::/7", // IPv6 unique local
|
||||
"fe80::/10", // IPv6 link-local
|
||||
"127.0.0.0/8",
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"::1/128",
|
||||
"fc00::/7",
|
||||
"fe80::/10",
|
||||
}
|
||||
for _, cidr := range privateCIDRs {
|
||||
_, ipNet, _ := net.ParseCIDR(cidr)
|
||||
@@ -107,6 +161,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check proxy authentication
|
||||
if tconn.HasProxyAuth() {
|
||||
if r.URL.Path == "/_drip/login" {
|
||||
h.handleProxyLogin(w, r, tconn, subdomain)
|
||||
return
|
||||
}
|
||||
if !h.isProxyAuthenticated(r, subdomain) {
|
||||
h.serveLoginPage(w, r, subdomain, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tType := tconn.GetTunnelType()
|
||||
if tType != "" && tType != protocol.TunnelTypeHTTP && tType != protocol.TunnelTypeHTTPS {
|
||||
http.Error(w, "Tunnel does not accept HTTP traffic", http.StatusBadGateway)
|
||||
@@ -638,3 +704,145 @@ type bufferedReadWriteCloser struct {
|
||||
func (b *bufferedReadWriteCloser) Read(p []byte) (int, error) {
|
||||
return b.Reader.Read(p)
|
||||
}
|
||||
|
||||
func (h *Handler) isProxyAuthenticated(r *http.Request, subdomain string) bool {
|
||||
cookie, err := r.Cookie(authCookieName + "_" + subdomain)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return sessionStore.validate(cookie.Value, subdomain)
|
||||
}
|
||||
|
||||
func (h *Handler) handleProxyLogin(w http.ResponseWriter, r *http.Request, tconn *tunnel.Connection, subdomain string) {
|
||||
if r.Method != http.MethodPost {
|
||||
h.serveLoginPage(w, r, subdomain, "")
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
h.serveLoginPage(w, r, subdomain, "Invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
password := r.FormValue("password")
|
||||
|
||||
if !tconn.ValidateProxyAuth(password) {
|
||||
h.serveLoginPage(w, r, subdomain, "Invalid password")
|
||||
return
|
||||
}
|
||||
|
||||
token := sessionStore.create(subdomain)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: authCookieName + "_" + subdomain,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
MaxAge: int(authSessionDuration.Seconds()),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
redirectURL := r.FormValue("redirect")
|
||||
if redirectURL == "" || redirectURL == "/_drip/login" {
|
||||
redirectURL = "/"
|
||||
}
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) serveLoginPage(w http.ResponseWriter, r *http.Request, subdomain string, errorMsg string) {
|
||||
redirectURL := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
redirectURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
if redirectURL == "/_drip/login" {
|
||||
redirectURL = "/"
|
||||
}
|
||||
|
||||
errorHTML := ""
|
||||
if errorMsg != "" {
|
||||
errorHTML = fmt.Sprintf(`<p class="error">%s</p>`, errorMsg)
|
||||
}
|
||||
|
||||
html := fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%s - Drip</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #fff;
|
||||
color: #24292f;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.container { max-width: 720px; margin: 0 auto; padding: 48px 24px; }
|
||||
header { margin-bottom: 48px; }
|
||||
h1 { font-size: 28px; font-weight: 600; margin-bottom: 8px; }
|
||||
h1 span { margin-right: 8px; }
|
||||
.desc { color: #57606a; font-size: 16px; }
|
||||
p { margin-bottom: 24px; }
|
||||
.error { color: #cf222e; margin-bottom: 16px; }
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
}
|
||||
.input-wrap input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
.input-wrap button {
|
||||
background: #24292f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.input-wrap button:hover { background: #32383f; }
|
||||
footer { margin-top: 48px; padding-top: 24px; border-top: 1px solid #d0d7de; }
|
||||
footer a { color: #57606a; text-decoration: none; font-size: 14px; }
|
||||
footer a:hover { color: #0969da; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><span>🔒</span>%s</h1>
|
||||
<p class="desc">This tunnel is password protected</p>
|
||||
</header>
|
||||
|
||||
%s
|
||||
<form method="POST" action="/_drip/login">
|
||||
<input type="hidden" name="redirect" value="%s" />
|
||||
<div class="input-wrap">
|
||||
<input type="password" name="password" placeholder="Enter password" required autofocus />
|
||||
<button type="submit">Continue</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<footer>
|
||||
<a href="https://github.com/Gouryella/drip" target="_blank">GitHub</a>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>`, subdomain, subdomain, errorHTML, redirectURL)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user