From 6a08d5af1c758c097de2a30e61e39681facb796b Mon Sep 17 00:00:00 2001 From: nahakubuilde Date: Fri, 18 Jul 2025 06:47:38 +0100 Subject: [PATCH] pwpush update --- .gitignore | 3 +- pwpusher.db | Bin 20480 -> 20480 bytes pwpusher/handler.go | 7 +- pwpusher/pwpusher.go | 311 +++++++++++++++++++++++++++++++++++-------- web/base.html | 121 +++++++++++++++++ web/pwview.html | 118 ++++++++++++++++ 6 files changed, 504 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 95485ac..148d85e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /go.mod /go.sum /ImageMagick -wordlist.txt \ No newline at end of file +wordlist.txt +*.db \ No newline at end of file diff --git a/pwpusher.db b/pwpusher.db index 12a2e8abbbc42a2a8800da7493bf6084c56fa99f..f71eddba6a13fe6ca3af3b7114053e7f40a31088 100644 GIT binary patch literal 20480 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 literal 20480 zcmeI(%W~RQ7yw{l;na9cluak&RWwsi8rKcza&+@driCCb0RiITfH#b;AdI*Ofgv+p zxSe*9PXs_(tL^l2v{&f^*P+j?Vubu{au0RSS-9k@moI zHJ@a$2UsYCeM^!UhV2CVX0Tsucu)vmRD#dYmGaw)J6P)8J?y21Fz)_hm&!~8gD+Y z$cdUv=87q~L0*p_OGR=O_2jbIzKY-I(wYq&&lSp!6ao$-iku1pPqamMcMF^sbWH_$*K&$XUOj#}s`bZRZPuDP*6BD9tH~L;a;>#7 z@jR>V--t-b=|sIyBdNszJZm%wxLQWfF;2*2sZ@{?Me-tqUW8~9f6y|mo)s*!*9Kh9 z#xlW08)tmp!o1aFdD@{i9-j_8v(?eYon@=-$o|2`y@%iIhOp(;F&_1Tb$x4b@@2iX znt&F)`dqB#w-y&$G}yyCKVZaz`$lvS6kr1Z5C8!X009sH0T2KI5CDOXCvfJ3ql#Ot zhPEP+M_Xr`f@qqei6~mv1Z1*0k0jn=B~zr3ZJRvPcBX@_J#Cxi^pH`#wr%tILUwXm zid&U*f1svEZl)-UM+#EigZ8s$48?FyDd8zCkPLmuF^3ct6A=<iuJV`;o}oCzNc7cA#ita%oZU4MpTC*hGdC1fCg&3r zT#Pnr>LO*(W-wgVFhp7uctK(*oe~Yr&~%yZJFkv})Unic%c)ll#T}G|sdMy-%Q^i@ z*5|6-c5+a0>y9+ClPS-fH@%P93nM2PY z00JNY0w4eaAOHd&00JNY0wC~-2<+iX=&rk4;YHA0w}tR!Pj}o%+4uriB6r=tSgrqc zjQE+*iNA>7K9QM#G(i9aKmY_l00ck)1V8`;KmY_l-~$VMiC07K@q9^mP29R?GVsT^ zdfwl=o@m&3j8`M8(_VzFeTrbU{(p@T{}TTYe-f`haPrU{2!H?xfB*=900@8p2!H?x gfB*=9z=srw;*Uaa{>2c!R>QB=Hg@nwkvEb51C|&AvH$=8 diff --git a/pwpusher/handler.go b/pwpusher/handler.go index 1ead6f6..b733af1 100644 --- a/pwpusher/handler.go +++ b/pwpusher/handler.go @@ -13,7 +13,10 @@ func (p *PWPusher) RegisterRoutes(mux *http.ServeMux) { // API endpoint for checking link status mux.HandleFunc("/pwpush/api/status/", p.StatusHandler) - // PWPusher sub-routes (push viewing) + // New short URL format for viewing + mux.HandleFunc("/s/", p.ViewHandler) + + // PWPusher sub-routes (push viewing) - for backward compatibility mux.HandleFunc("/pwpush/", func(w http.ResponseWriter, r *http.Request) { // Extract push ID from URL path like /pwpush/abc123 id := strings.TrimPrefix(r.URL.Path, "/pwpush/") @@ -26,7 +29,7 @@ func (p *PWPusher) RegisterRoutes(mux *http.ServeMux) { } }) - // Direct view handler for clean URLs + // Direct view handler for clean URLs - for backward compatibility mux.HandleFunc("/pwview/", p.ViewHandler) } diff --git a/pwpusher/pwpusher.go b/pwpusher/pwpusher.go index 69a191b..1e74fc7 100644 --- a/pwpusher/pwpusher.go +++ b/pwpusher/pwpusher.go @@ -7,7 +7,6 @@ import ( "crypto/sha256" "database/sql" "encoding/base64" - "encoding/hex" "encoding/json" "fmt" "html" @@ -269,9 +268,33 @@ func loadViewTemplates(embeddedFS fs.FS) (*template.Template, error) { } func (p *PWPusher) generateID() string { - bytes := make([]byte, 16) + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + // Generate random length between 10-16 characters + length := 10 + (int(p.randomByte()) % 7) // 10 + 0-6 = 10-16 + + bytes := make([]byte, length) + for i := range bytes { + bytes[i] = charset[p.randomByte()%byte(len(charset))] + } + return string(bytes) +} + +func (p *PWPusher) generateEncryptionKey() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + // Generate random length between 5-10 characters (URL safe) + length := 5 + (int(p.randomByte()) % 6) // 5 + 0-5 = 5-10 + + bytes := make([]byte, length) + for i := range bytes { + bytes[i] = charset[p.randomByte()%byte(len(charset))] + } + return string(bytes) +} + +func (p *PWPusher) randomByte() byte { + bytes := make([]byte, 1) rand.Read(bytes) - return hex.EncodeToString(bytes) + return bytes[0] } func (p *PWPusher) encrypt(text string) (string, error) { @@ -324,6 +347,71 @@ func (p *PWPusher) decrypt(encryptedText string) (string, error) { return string(plaintext), nil } +// encryptWithKey encrypts text with an additional key layer +func (p *PWPusher) encryptWithKey(text, key string) (string, error) { + // First encrypt with the PWPusher's main encryption key + firstEncryption, err := p.encrypt(text) + if err != nil { + return "", err + } + + // Create a hash of the additional key for AES + keyHash := sha256.Sum256([]byte(key)) + + block, err := aes.NewCipher(keyHash[:]) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + + ciphertext := gcm.Seal(nonce, nonce, []byte(firstEncryption), nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// decryptWithKey decrypts text that was encrypted with an additional key layer +func (p *PWPusher) decryptWithKey(encryptedText, key string) (string, error) { + data, err := base64.StdEncoding.DecodeString(encryptedText) + if err != nil { + return "", err + } + + // Create a hash of the additional key for AES + keyHash := sha256.Sum256([]byte(key)) + + block, err := aes.NewCipher(keyHash[:]) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return "", fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + firstDecryption, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", err + } + + // Now decrypt with the PWPusher's main encryption key + return p.decrypt(string(firstDecryption)) +} + // Rate limiting methods for password attempts func (p *PWPusher) isBlocked(clientIP string) bool { if attempts, exists := p.failedAttempts[clientIP]; exists { @@ -508,8 +596,11 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) { return } - // Encrypt text - encryptedText, err := p.encrypt(req.Text) + // Generate additional encryption key + additionalKey := p.generateEncryptionKey() + + // Encrypt text with double encryption + encryptedText, err := p.encryptWithKey(req.Text, additionalKey) if err != nil { log.Printf("Encryption error: %v", err) http.Error(w, "Failed to encrypt text", http.StatusInternalServerError) @@ -547,12 +638,12 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) { return } - // Prepare response URL + // Prepare response URL with new format scheme := "http" if r.TLS != nil { scheme = "https" } - fullURL := fmt.Sprintf("%s://%s/pwview/%s", scheme, r.Host, id) + fullURL := fmt.Sprintf("%s://%s/s/%s?k=%s", scheme, r.Host, id, additionalKey) // Save to user's history if tracking is enabled if req.TrackHistory { @@ -583,21 +674,47 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) { func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { // Extract ID from URL path - var path string - if strings.HasPrefix(r.URL.Path, "/pwview/") { - path = strings.TrimPrefix(r.URL.Path, "/pwview/") - } else if strings.HasPrefix(r.URL.Path, "/pwpush/view/") { - path = strings.TrimPrefix(r.URL.Path, "/pwpush/view/") - } else { - path = strings.TrimPrefix(r.URL.Path, "/pwpush/") - } + var id string + var encryptionKey string - if path == "" { - http.Error(w, "Invalid push ID", http.StatusBadRequest) + if strings.HasPrefix(r.URL.Path, "/s/") { + // New format: /s/?k= + id = strings.TrimPrefix(r.URL.Path, "/s/") + encryptionKey = r.URL.Query().Get("k") + + if encryptionKey == "" { + // Redirect to PWPusher main page with error popup + errorMsg := url.QueryEscape("Missing encryption key. Please use the complete secure link.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) + return + } + } else if strings.HasPrefix(r.URL.Path, "/pwview/") { + // Legacy format: /pwview/ + // For legacy URLs, we'll show an error since they don't have the encryption key + errorMsg := url.QueryEscape("This link format is no longer supported. Please use the new secure link format.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) + return + } else if strings.HasPrefix(r.URL.Path, "/pwpush/view/") { + // Legacy format: /pwpush/view/ + // For legacy URLs, we'll show an error since they don't have the encryption key + errorMsg := url.QueryEscape("This link format is no longer supported. Please use the new secure link format.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) + return + } else { + // Legacy format: /pwpush/ + // For legacy URLs, we'll show an error since they don't have the encryption key + errorMsg := url.QueryEscape("This link format is no longer supported. Please use the new secure link format.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) return } - log.Printf("ViewHandler: Extracted ID '%s' from URL '%s'", path, r.URL.Path) + if id == "" { + errorMsg := url.QueryEscape("Invalid push ID.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) + return + } + + log.Printf("ViewHandler: Extracted ID '%s' from URL '%s'", id, r.URL.Path) // Handle POST requests (reveal actions and password verification) if r.Method == http.MethodPost { @@ -606,15 +723,74 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { // Validate CSRF token for form submissions csrfToken := r.FormValue("csrf_token") if !p.validateCSRFToken(csrfToken) { - http.Error(w, "Invalid CSRF token", http.StatusForbidden) + // Redirect with error popup instead of showing empty error page + errorMsg := url.QueryEscape("Invalid or expired security token. Please try again.") + redirectURL := fmt.Sprintf("%s?k=%s&error=%s", r.URL.Path, encryptionKey, errorMsg) + http.Redirect(w, r, redirectURL, http.StatusSeeOther) return } action := r.FormValue("action") if action == "reveal" { - // Redirect to same URL with show=true parameter - http.Redirect(w, r, r.URL.Path+"?show=true", http.StatusSeeOther) + // Handle reveal action - decrypt and return content directly + // Get push data + push, err := p.getPushByID(id) + if err != nil { + // Redirect with error popup instead of empty page + errorMsg := url.QueryEscape("Link not found or has been deleted.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) + return + } + + // Check if expired + if time.Now().After(push.ExpiresAt) || push.IsDeleted { + p.renderTemplate(w, "pwview.html", ViewData{ + CurrentPage: "pwpush", + Error: "This link has expired or been deleted.", + }) + return + } + + // Check if max views reached + if push.CurrentViews >= push.MaxViews { + p.renderTemplate(w, "pwview.html", ViewData{ + CurrentPage: "pwpush", + Error: "This link has reached its maximum view limit.", + }) + return + } + + // Increment view count + _, err = p.db.Exec("UPDATE pushes SET current_views = current_views + 1 WHERE id = ?", id) + if err != nil { + log.Printf("Failed to increment view count: %v", err) + } + + // Refresh push data to get updated view count + push, err = p.getPushByID(id) + if err != nil { + log.Printf("Failed to get updated push data: %v", err) + } + + // Decrypt text with the encryption key + text, err := p.decryptWithKey(push.EncryptedText, encryptionKey) + if err != nil { + log.Printf("Failed to decrypt text: %v", err) + // Redirect with error popup instead of empty page + errorMsg := url.QueryEscape("Failed to decrypt content - invalid encryption key. Please check your link.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) + return + } + + // Render the text with delete option if auto-delete is enabled + p.renderTemplate(w, "pwview.html", ViewData{ + CurrentPage: "pwpush", + Push: push, + Text: text, + ShowText: true, + Revealed: true, + }) return } else if action == "verify_password" { // Handle password verification with rate limiting @@ -627,7 +803,7 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { log.Printf("IP %s is blocked until %v", clientIP, blockedUntil) p.renderTemplate(w, "pwview.html", ViewData{ CurrentPage: "pwpush", - Push: &PushData{ID: path}, // Minimal push data for display + Push: &PushData{ID: id}, // Minimal push data for display RequirePassword: true, IsBlocked: true, BlockedUntil: blockedUntil, @@ -639,9 +815,11 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") // Get push data to check password - push, err := p.getPushByID(path) + push, err := p.getPushByID(id) if err != nil { - http.Error(w, "Link not found", http.StatusNotFound) + // Redirect with error popup instead of empty page + errorMsg := url.QueryEscape("Link not found or has been deleted.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) return } @@ -675,14 +853,44 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { } } - // Password is correct - reset failed attempts and redirect + // Password is correct - reset failed attempts and show content directly log.Printf("Correct password for IP %s, resetting attempts", clientIP) p.resetFailedAttempts(clientIP) - http.Redirect(w, r, r.URL.Path+"?show=true&verified=true", http.StatusSeeOther) + + // Decrypt text with the encryption key + text, err := p.decryptWithKey(push.EncryptedText, encryptionKey) + if err != nil { + log.Printf("Failed to decrypt text: %v", err) + // Redirect with error popup instead of empty page + errorMsg := url.QueryEscape("Failed to decrypt content - invalid encryption key. Please check your link.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) + return + } + + // Increment view count since password is correct + _, err = p.db.Exec("UPDATE pushes SET current_views = current_views + 1 WHERE id = ?", id) + if err != nil { + log.Printf("Failed to increment view count: %v", err) + } + + // Refresh push data to get updated view count + push, err = p.getPushByID(id) + if err != nil { + log.Printf("Failed to get updated push data: %v", err) + } + + // Render the text directly (no click required since password was verified) + p.renderTemplate(w, "pwview.html", ViewData{ + CurrentPage: "pwpush", + Push: push, + Text: text, + ShowText: true, + Revealed: true, + }) return } else if action == "delete" { // Manual delete action - _, err := p.db.Exec("UPDATE pushes SET is_deleted = 1 WHERE id = ?", path) + _, err := p.db.Exec("UPDATE pushes SET is_deleted = 1 WHERE id = ?", id) if err != nil { log.Printf("Failed to mark as deleted: %v", err) } @@ -694,11 +902,8 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { } } - // Check if this is a reveal request - showText := r.URL.Query().Get("show") == "true" - // Get push data - push, err := p.getPushByID(path) + push, err := p.getPushByID(id) if err != nil { if err == sql.ErrNoRows { p.renderTemplate(w, "pwview.html", ViewData{ @@ -731,8 +936,7 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { } // Check if password is required and not yet verified - passwordVerified := r.URL.Query().Get("verified") == "true" - if push.PasswordHash != "" && !passwordVerified { + if push.PasswordHash != "" { clientIP := p.getClientIP(r) // Check if client is blocked @@ -760,8 +964,8 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { return } - // If require click is enabled and showText is not true, show the reveal page - if push.RequireClick && !showText && push.PasswordHash == "" { + // If require click is enabled, show the reveal page (only for GET requests) + if push.RequireClick { p.renderTemplate(w, "pwview.html", ViewData{ CurrentPage: "pwpush", Push: push, @@ -771,25 +975,26 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { return } - // Increment view count only when actually viewing content - if showText || !push.RequireClick { - _, err = p.db.Exec("UPDATE pushes SET current_views = current_views + 1 WHERE id = ?", path) - if err != nil { - log.Printf("Failed to increment view count: %v", err) - } - - // Refresh push data to get updated view count - push, err = p.getPushByID(path) - if err != nil { - log.Printf("Failed to get updated push data: %v", err) - } + // If no restrictions (no password, no click required), show content directly + // Increment view count + _, err = p.db.Exec("UPDATE pushes SET current_views = current_views + 1 WHERE id = ?", id) + if err != nil { + log.Printf("Failed to increment view count: %v", err) } - // Decrypt text - text, err := p.decrypt(push.EncryptedText) + // Refresh push data to get updated view count + push, err = p.getPushByID(id) + if err != nil { + log.Printf("Failed to get updated push data: %v", err) + } + + // Decrypt text with the encryption key + text, err := p.decryptWithKey(push.EncryptedText, encryptionKey) if err != nil { log.Printf("Failed to decrypt text: %v", err) - http.Error(w, "Failed to decrypt content", http.StatusInternalServerError) + // Redirect with error popup instead of empty page + errorMsg := url.QueryEscape("Failed to decrypt content - invalid encryption key. Please check your link.") + http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther) return } @@ -898,9 +1103,9 @@ func (p *PWPusher) validateAndSanitizeText(text string) (string, error) { return "", fmt.Errorf("text contains invalid UTF-8 characters") } - // Sanitize HTML - sanitized := html.EscapeString(text) - return sanitized, nil + // Store the original text without HTML escaping + // The template will handle safe display + return text, nil } func (p *PWPusher) validatePassword(password string) error { diff --git a/web/base.html b/web/base.html index 9bb659f..5355a2f 100644 --- a/web/base.html +++ b/web/base.html @@ -6,6 +6,61 @@ {{block "title" .}}HeaderAnalyzer{{end}} + {{block "head" .}}{{end}} @@ -25,6 +80,72 @@ {{end}} + + + + + {{block "scripts" .}}{{end}} diff --git a/web/pwview.html b/web/pwview.html index 9cf6f48..4e10b5e 100644 --- a/web/pwview.html +++ b/web/pwview.html @@ -300,6 +300,60 @@ box-shadow: 0 0 0 3px rgba(60, 92, 124, 0.3); outline: none; } + + /* Popup notification styles */ + .popup-notification { + position: fixed; + top: 20px; + right: 20px; + z-index: 10000; + background: #f44336; + color: white; + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + max-width: 400px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + transform: translateX(100%); + transition: transform 0.3s ease-in-out; + } + + .popup-notification.show { + transform: translateX(0); + } + + .popup-notification.error { + background: #f44336; + } + + .popup-notification.warning { + background: #ff9800; + } + + .popup-notification.success { + background: #4caf50; + } + + .popup-notification.info { + background: #2196f3; + } + + .popup-notification .close-btn { + float: right; + background: none; + border: none; + color: white; + font-size: 18px; + font-weight: bold; + cursor: pointer; + margin-left: 10px; + padding: 0; + line-height: 1; + } + + .popup-notification .close-btn:hover { + opacity: 0.7; + } @@ -496,8 +550,72 @@ document.addEventListener('DOMContentLoaded', function() { // Auto-select short content setTimeout(() => selectAll(), 500); } + + // Check for error parameters when page loads + checkForErrorParams(); }); + +// Popup notification system +function showPopup(message, type = 'error', duration = 5000) { + const container = document.getElementById('popup-container'); + const popup = document.createElement('div'); + popup.className = `popup-notification ${type}`; + popup.innerHTML = ` + + ${message} + `; + + container.appendChild(popup); + + // Trigger animation + setTimeout(() => popup.classList.add('show'), 10); + + // Auto-remove after duration + if (duration > 0) { + setTimeout(() => { + popup.classList.remove('show'); + setTimeout(() => popup.remove(), 300); + }, duration); + } +} + +// Show popup from URL parameters (for redirects) +function checkForErrorParams() { + const urlParams = new URLSearchParams(window.location.search); + const error = urlParams.get('error'); + const warning = urlParams.get('warning'); + const success = urlParams.get('success'); + const info = urlParams.get('info'); + + if (error) { + showPopup(decodeURIComponent(error), 'error'); + // Clean URL + urlParams.delete('error'); + window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`); + } + + if (warning) { + showPopup(decodeURIComponent(warning), 'warning'); + urlParams.delete('warning'); + window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`); + } + + if (success) { + showPopup(decodeURIComponent(success), 'success'); + urlParams.delete('success'); + window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`); + } + + if (info) { + showPopup(decodeURIComponent(info), 'info'); + urlParams.delete('info'); + window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`); + } +} + + +