Commit: eb8fe7aadf7993ed40881c8ed50e75a9488ce044 Author: Vi Grey Date: 2024-06-18 06:47 UTC Summary: Initial commit .gitignore | 1 + CHANGELOG.txt | 8 ++++ Makefile | 37 +++++++++++++++++ allow-low-ports.sh | 6 +++ go.mod | 3 ++ src/augelmir.go | 19 +++++++++ src/config.go | 67 ++++++++++++++++++++++++++++++ src/http.go | 175 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 316 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..eff26a0 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,8 @@ +# Change Log +All notable changes to this project will be documented in this file. + + +## [0.0.1] - 2024-06-18 + +### Added +- Initial release diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..26da969 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +# Copyright (C) 2024, Vi Grey +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +PKG_NAME := aurgelmir +CURRENTDIR := $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) + +all: + mkdir -p $(CURRENTDIR)build/bin; \ + cd $(CURRENTDIR)src; \ + go build -ldflags="-s -w" -o $(CURRENTDIR)build/bin/$(PKG_NAME); \ + cd $(CURRENTDIR); \ + #upx --brute $(CURRENTDIR)build/bin/$(PKG_NAME); \ + +clean: + rm -rf -- $(CURRENTDIR)build; \ diff --git a/allow-low-ports.sh b/allow-low-ports.sh new file mode 100755 index 0000000..c41a423 --- /dev/null +++ b/allow-low-ports.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [ $# -eq 0 ]; then + >&2 echo "Missing path to Aurgelmir binary as argument" + exit 1 +fi +setcap CAP_NET_BIND_SERVICE=+eip "$1" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2b50056 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module aurgelmir + +go 1.22.0 diff --git a/src/augelmir.go b/src/augelmir.go new file mode 100644 index 0000000..dac640c --- /dev/null +++ b/src/augelmir.go @@ -0,0 +1,19 @@ +package main + +import ( + "io" + "log" +) + +const ( + VERSION = "0.0.1" +) + +func init() { + log.SetOutput(io.Discard) + parseConfigData() +} + +func main() { + startHTTPServer() +} diff --git a/src/config.go b/src/config.go new file mode 100644 index 0000000..7f166ba --- /dev/null +++ b/src/config.go @@ -0,0 +1,67 @@ +package main + +import ( + "encoding/json" + "fmt" + "mime" + "os" +) + +const ( + CONFIG_FILE_PATH = "config.json" +) + +var ( + configData Config +) + +type Config struct { + AurgelmirVersion string `json:"aurgelmir_version"` + Address string `json:"address"` + TLSAddress string `json:"tls_address"` + TLSCert string `json:"tls_cert"` + TLSKey string `json:"tls_key"` + TLSMinVersion string `json:"tls_min_version"` + ContentTypes []ContentType `json:"content_types"` + Matches []Match `json:"matches"` +} + +type Match struct { + Input string `json:"input"` + Expression string `json:"expression"` + Index bool `json:"index"` + OptionalHTMLExt bool `json:"optional_html_ext"` + SetHeaders []Header `json:"set_headers"` + WebRoots []string `json:"web_roots"` + ProxyAddress string `json:"proxy_address"` + SetStatus int `json:"set_status"` + Close bool `json:"close"` +} + +type ContentType struct { + Ext string `json:"ext"` + Type string `json:"type"` +} + +type Header struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// Open config file and parse contents into configData +func parseConfigData() { + c, err := os.Open(CONFIG_FILE_PATH) + if err != nil { + fmt.Printf("Unable to open config file at %s\n", CONFIG_FILE_PATH) + } + defer c.Close() + configData = Config{} + err = json.NewDecoder(c).Decode(&configData) + if err != nil { + fmt.Printf("Unable to parse config file %s\n", CONFIG_FILE_PATH) + } + // Add all content types to mime type database + for _, contentType := range configData.ContentTypes { + mime.AddExtensionType(contentType.Ext, contentType.Type) + } +} diff --git a/src/http.go b/src/http.go new file mode 100644 index 0000000..27de702 --- /dev/null +++ b/src/http.go @@ -0,0 +1,175 @@ +package main + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path" + "regexp" + "strings" +) + +var ( + httpStatuses = []int{ + 100, 101, 102, 103, + 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, + 300, 301, 302, 303, 304, 305, 307, 308, + 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, + 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, + 422, 423, 424, 425, 426, 428, 429, 431, 451, + 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, + } + tlsVersions = map[string]uint16{ + "1.2": 0x0303, + "1.3": 0x0304, + } +) + +func httpHandler(w http.ResponseWriter, r *http.Request) { + var webRoots []string + var headers []Header + var status int + var index, optionalHTMLExt bool + var proxyAddress string + for _, match := range configData.Matches { + var input string + switch match.Input { + case "referer": + input = r.Referer() + case "user-agent": + input = r.UserAgent() + case "host": + input = r.Host + case "path": + input = r.URL.Path + } + expressionMatches := regexp.MustCompile(match.Expression).MatchString(input) + if !expressionMatches { + continue + } + webRoots = append(webRoots, match.WebRoots...) + headers = append(headers, match.SetHeaders...) + status = match.SetStatus + index = match.Index + optionalHTMLExt = match.OptionalHTMLExt + proxyAddress = match.ProxyAddress + if match.Close { + break + } + } + var validStatus bool + for _, s := range httpStatuses { + if status == s { + validStatus = true + break + } + } + // If no status set and no web_roots and no proxy_address, kill TCP connection + // If error, close HTTP connection "normally" + if !validStatus && len(webRoots) == 0 && proxyAddress == "" { + // Create HTTP hijacker + if hijacker, ok := w.(http.Hijacker); ok { + // Get raw TCP connection of HTTP connection + if conn, _, err := hijacker.Hijack(); err == nil { + // Kill connection + conn.Close() + return + } + } + r.Body.Close() + return + } + if proxyAddress != "" { + serveProxy(w, r, proxyAddress) + return + } + for _, header := range headers { + w.Header().Set(header.Key, header.Value) + } + if validStatus { + w.WriteHeader(status) + } + if len(webRoots) > 0 { + serveFile(w, r, webRoots, index, optionalHTMLExt) + return + } +} + +func serveFile(w http.ResponseWriter, r *http.Request, webRoots []string, index, optionalHTMLExt bool) { + var tryPaths []string + for _, webRoot := range webRoots { + unescapedPath, _ := url.PathUnescape(r.URL.Path) + isDir := strings.HasSuffix(unescapedPath, "/") + cleanPath := path.Clean(unescapedPath) + fullPath := path.Join(webRoot, cleanPath) + if !isDir { + tryPaths = append(tryPaths, fullPath) + if optionalHTMLExt { + tryPaths = append(tryPaths, fullPath+".html") + } + } + if index { + tryPaths = append(tryPaths, path.Join(fullPath, "index.html")) + } + } + for _, t := range tryPaths { + if _, err := os.Stat(t); err == nil { + http.ServeFile(w, r, t) + return + } + } + w.WriteHeader(404) +} + +func newProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + return httputil.NewSingleHostReverseProxy(url), nil +} + +func serveProxy(w http.ResponseWriter, r *http.Request, proxyAddress string) { + proxy, err := newProxy(proxyAddress) + if err != nil { + w.WriteHeader(500) + return + } + // Set X-Real-IP and X-Forwarded-For headers + ipAddr, _, _ := net.SplitHostPort(r.RemoteAddr) + r.Header.Set("X-Real-IP", ipAddr) + r.Header.Set("X-Forwarded-For", ipAddr) + proxy.ServeHTTP(w, r) +} + +func startHTTPServer() { + http.HandleFunc("/", httpHandler) + + go func() { + err := http.ListenAndServe(configData.Address, nil) + if err != nil { + fmt.Println("Unable to start HTTP server", err) + } + }() + if configData.TLSAddress != "" { + tlsMinVersion := tlsVersions["1.2"] + if tlsVersions[configData.TLSMinVersion] != 0 { + tlsMinVersion = tlsVersions[configData.TLSMinVersion] + } + tlsServer := &http.Server{ + Addr: configData.TLSAddress, + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), + TLSConfig: &tls.Config{ + MinVersion: tlsMinVersion, + }, + } + err := tlsServer.ListenAndServeTLS(configData.TLSCert, configData.TLSKey) + if err != nil { + fmt.Println("Unable to start HTTPS server", err) + } + } +}