forgejo-pages-proxy/proxy.go
Arija A. b7a5459922
Add permissions policy in CLI
Signed-off-by: Arija A. <ari@ari.lt>
2026-04-25 00:46:36 +03:00

214 lines
6 KiB
Go

// Copyright (C) 2026 VšĮ „0011.LT"
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 ONLY.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
package main
import (
"bufio"
"fmt"
"io"
"log"
"mime"
"net/http"
"path"
"strings"
)
func (server *app) serve_http(response_writer http.ResponseWriter, request *http.Request) {
if !is_pages_path(request.URL.Path) {
write_plain_text_error(response_writer, request, http.StatusNotFound, "HTTP error 404")
return
}
upstream_paths, ok := build_upstream_paths(request.URL.Path)
if !ok {
write_plain_text_error(response_writer, request, http.StatusNotFound, "HTTP error 404")
return
}
for idx, upstream_path := range upstream_paths {
is_last_candidate := idx == len(upstream_paths)-1
handled, err := server.proxy_candidate(response_writer, request, upstream_path, is_last_candidate)
if err != nil {
log.Printf("proxy error for %s: %v", upstream_path, err)
http.Error(response_writer, "HTTP error 502", http.StatusBadGateway)
return
}
if handled {
return
}
}
write_plain_text_error(response_writer, request, http.StatusNotFound, "HTTP error 404")
}
func (server *app) proxy_candidate(
response_writer http.ResponseWriter,
request *http.Request,
upstream_path string,
is_last_candidate bool,
) (bool, error) {
target_url := *server.base_url
target_url.Path = join_paths(server.base_url.Path, upstream_path)
target_url.RawQuery = request.URL.RawQuery
target_url.Fragment = ""
upstream_request, err := http.NewRequestWithContext(request.Context(), request.Method, target_url.String(), nil)
if err != nil {
return false, err
}
copy_request_headers(upstream_request.Header, request.Header)
upstream_request.Host = server.base_url.Host
upstream_response, err := server.http_client.Do(upstream_request)
if err != nil {
return false, err
}
defer upstream_response.Body.Close()
if upstream_response.StatusCode == http.StatusNotFound {
if is_last_candidate {
write_plain_text_error(response_writer, request, http.StatusNotFound, "HTTP error 404")
return true, nil
}
return false, nil
}
copy_response_headers(response_writer.Header(), upstream_response.Header)
set_security_headers(response_writer.Header())
if upstream_response.StatusCode == http.StatusNotModified {
response_writer.WriteHeader(http.StatusNotModified)
return true, nil
}
response_body_reader := bufio.NewReader(upstream_response.Body)
content_type := determine_content_type(upstream_path, upstream_response.Header.Get("Content-Type"), response_body_reader)
response_writer.Header().Set("Content-Type", content_type)
if is_html_content_type(content_type, upstream_path) && server.csp != "" {
response_writer.Header().Set("Content-Security-Policy", server.csp)
}
if is_html_content_type(content_type, upstream_path) && server.pp != "" {
response_writer.Header().Set("Permissions-Policy", server.pp)
}
response_writer.WriteHeader(upstream_response.StatusCode)
if request.Method == http.MethodHead {
return true, nil
}
_, err = io.Copy(response_writer, response_body_reader)
if err != nil {
return true, err
}
return true, nil
}
func determine_content_type(upstream_path string, upstream_content_type string, body_reader *bufio.Reader) string {
extension_content_type := mime.TypeByExtension(strings.ToLower(path.Ext(upstream_path)))
if extension_content_type != "" {
return extension_content_type
}
header_content_type := strings.TrimSpace(upstream_content_type)
if header_content_type != "" &&
!strings.HasPrefix(header_content_type, "text/plain") &&
!strings.HasPrefix(header_content_type, "application/octet-stream") {
return header_content_type
}
peeked_bytes, _ := body_reader.Peek(512)
if len(peeked_bytes) > 0 {
sniffed_content_type := http.DetectContentType(peeked_bytes)
if strings.HasPrefix(sniffed_content_type, "text/html") {
return "text/html; charset=utf-8"
}
return sniffed_content_type
}
return "application/octet-stream"
}
func is_html_content_type(content_type string, upstream_path string) bool {
if strings.Contains(strings.ToLower(content_type), "text/html") {
return true
}
return strings.EqualFold(path.Ext(upstream_path), ".html")
}
func copy_request_headers(destination_headers http.Header, source_headers http.Header) {
for header_name, header_values := range source_headers {
if is_skippable_header(header_name) {
continue
}
for _, header_value := range header_values {
destination_headers.Add(header_name, header_value)
}
}
destination_headers.Del("Accept-Encoding")
}
func copy_response_headers(destination_headers http.Header, source_headers http.Header) {
for header_name, header_values := range source_headers {
if is_skippable_header(header_name) {
continue
}
for _, header_value := range header_values {
destination_headers.Add(header_name, header_value)
}
}
}
func is_skippable_header(header_name string) bool {
switch strings.ToLower(header_name) {
case
"connection",
"proxy-connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"set-cookie",
"content-security-policy",
"x-content-type-options",
"referrer-policy",
"x-frame-options",
"cross-origin-resource-policy",
"cross-origin-opener-policy",
"permissions-policy",
"upgrade":
return true
default:
return false
}
}
func write_plain_text_error(response_writer http.ResponseWriter, request *http.Request, status_code int, message string) {
response_writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
response_writer.WriteHeader(status_code)
if request.Method == http.MethodHead {
return
}
fmt.Fprintln(response_writer, message)
}