Compare commits

...

8 Commits

Author SHA1 Message Date
Captain ALM f5f1625533
Add Gitbucket support. 2023-12-10 14:30:37 +00:00
Captain ALM 2e11c55981
Add setup and deploy support.
Fix readme.
2023-12-01 23:33:31 +00:00
Captain ALM 59b5d686d4
Simple Empty Content Fix.
continuous-integration/drone/push Build is passing Details
2022-07-15 15:33:21 +01:00
Captain ALM 1a48e7815f
Make sure no content length is outputted in non-content circumstances.
continuous-integration/drone/push Build is passing Details
2022-07-15 15:25:50 +01:00
Captain ALM 3910d29fa2
Allow the config file to be specified in the .env file.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-07-14 22:00:10 +01:00
Captain ALM 976d356398
Add a path return limit for the git import system to be limited to the package name length in path entries.
continuous-integration/drone/push Build is passing Details
2022-07-14 21:58:11 +01:00
Captain ALM 0d4036d05c
Refactor and add cache and range support.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-07-12 15:41:50 +01:00
Captain ALM 8c67a34250
Refractor project.
continuous-integration/drone/push Build is passing Details
2022-07-12 14:19:02 +01:00
24 changed files with 751 additions and 215 deletions

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
</project>

View File

@ -1,18 +1,21 @@
SHELL := /bin/bash SHELL := /bin/bash
BIN := dist/gopkghsrv PRODUCT_NAME := gopkghsrv
ENTRY_POINT := ./cmd/gopkghsrv BIN := dist/${PRODUCT_NAME}
DNAME := ${PRODUCT_NAME}_
ENTRY_POINT := ./cmd/${PRODUCT_NAME}
HASH := $(shell git rev-parse --short HEAD) HASH := $(shell git rev-parse --short HEAD)
COMMIT_DATE := $(shell git show -s --format=%ci ${HASH}) COMMIT_DATE := $(shell git show -s --format=%ci ${HASH})
BUILD_DATE := $(shell date '+%Y-%m-%d %H:%M:%S') BUILD_DATE := $(shell date '+%Y-%m-%d %H:%M:%S')
VERSION := ${HASH} VERSION := ${HASH}
LD_FLAGS := -s -w -X 'main.buildVersion=${VERSION}' -X 'main.buildDate=${BUILD_DATE}' LD_FLAGS := -s -w -X 'main.buildVersion=${VERSION}' -X 'main.buildDate=${BUILD_DATE}' -X 'main.buildName=${PRODUCT_NAME}'
COMP_BIN := go COMP_BIN := go
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
BIN := $(BIN).exe BIN := $(BIN).exe
DNAME := $(DNAME).exe
endif endif
.PHONY: build dev test clean .PHONY: build dev test clean deploy d setup s
build: build:
mkdir -p dist/ mkdir -p dist/
@ -24,8 +27,30 @@ dev:
./${BIN} ./${BIN}
test: test:
go test ${COMP_BIN} test
clean: clean:
go clean ${COMP_BIN} clean
rm -r -f dist/ rm -r -f dist/
setup:
sudo cp "${PRODUCT_NAME}.service" /etc/systemd/system
sudo mkdir -p "/etc/${PRODUCT_NAME}"
sudo touch "/etc/${PRODUCT_NAME}/.env"
sudo systemctl daemon-reload
s:
sudo cp "${DNAME}.service" /etc/systemd/system
sudo mkdir -p "/etc/${DNAME}"
sudo touch "/etc/${DNAME}/.env"
sudo systemctl daemon-reload
deploy: build
sudo systemctl stop "${PRODUCT_NAME}"
sudo cp "${BIN}" /usr/local/bin
sudo systemctl start "${PRODUCT_NAME}"
d: build
sudo systemctl stop "${DNAME}"
sudo cp "${BIN}" "/usr/local/bin/${DNAME}"
sudo systemctl start "${DNAME}"

View File

@ -1,6 +1,6 @@
# GO Package Header Server # GO Package Header Server
[![Build Status](https://ci.mrmelon54.xyz/api/badges/alfred/GOPackageHeaderServer/status.svg)](https://ci.mrmelon54.xyz/alfred/GOPackageHeaderServer) [![Build Status](https://ci.mrmelon54.com/api/badges/alfred/GOPackageHeaderServer/status.svg)](https://ci.mrmelon54.com/alfred/GOPackageHeaderServer)
This allows for the required meta headers to be outputted in order for the GO package system to find the source files of the package. This allows for the required meta headers to be outputted in order for the GO package system to find the source files of the package.
@ -8,11 +8,11 @@ The outputter can be configured in runtime, the server has a YAML configuration.
The outputter can be used to add the extra meta tags to the head of the HTML document. The outputter can be used to add the extra meta tags to the head of the HTML document.
Maintainer: Maintainer:
[Captain ALM](https://code.mrmelon54.xyz/alfred) [Captain ALM](https://code.mrmelon54.com/alfred)
License: License:
[BSD 3-Clause](https://code.mrmelon54.xyz/alfred/GOPackageHeaderServer/src/branch/master/LICENSE.md) [BSD 3-Clause](https://code.mrmelon54.com/alfred/GOPackageHeaderServer/src/branch/master/LICENSE.md)
Example configuration: Example configuration:
[config.example.yml](https://code.mrmelon54.xyz/alfred/GOPackageHeaderServer/src/branch/master/config.example.yml) [config.example.yml](https://code.mrmelon54.com/alfred/GOPackageHeaderServer/src/branch/master/config.example.yml)
The configuration must by placed in a .data sub-directory from the executable. A .env file must also be generated (Can be empty). The configuration must by placed in a .data sub-directory from the executable. A .env file must also be generated (Can be empty).

View File

@ -10,6 +10,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"path" "path"
"path/filepath"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@ -48,8 +49,18 @@ func main() {
check(os.MkdirAll(dataDir, 0777)) check(os.MkdirAll(dataDir, 0777))
//Config file processing:
configLocation := os.Getenv("CONFIG_FILE")
if configLocation == "" {
configLocation = path.Join(dataDir, "config.yml")
} else {
if !filepath.IsAbs(configLocation) {
configLocation = path.Join(dataDir, configLocation)
}
}
//Config loading: //Config loading:
configFile, err := os.Open(path.Join(dataDir, "config.yml")) configFile, err := os.Open(configLocation)
if err != nil { if err != nil {
log.Fatalln("Failed to open config.yml") log.Fatalln("Failed to open config.yml")
} }

7
conf/cache.go Normal file
View File

@ -0,0 +1,7 @@
package conf
type CacheSettingsYaml struct {
MaxAge uint `yaml:"maxAge"`
NotModifiedResponseUsingLastModified bool `yaml:"notModifiedUsingLastModified"`
NotModifiedResponseUsingETags bool `yaml:"notModifiedUsingETags"`
}

View File

@ -3,16 +3,21 @@ package conf
import "golang.captainalm.com/GOPackageHeaderServer/outputMeta" import "golang.captainalm.com/GOPackageHeaderServer/outputMeta"
type ZoneYaml struct { type ZoneYaml struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Domains []string `yaml:"domains"` Domains []string `yaml:"domains"`
CssURL string `yaml:"cssURL"` CssURL string `yaml:"cssURL"`
HavePageContents bool `yaml:"havePageContents"` HavePageContents bool `yaml:"havePageContents"`
BasePath string `yaml:"basePath"` BasePath string `yaml:"basePath"`
UsernameProvided bool `yaml:"usernameProvided"` UsernameProvided bool `yaml:"usernameProvided"` //If set, the outputter will do /{user}/{repo}/ for repos rather than /{repo}/ ; Should really be named usernameProvidedByRequest
Username string `yaml:"username"` Username string `yaml:"username"`
BasePrefixURL string `yaml:"basePrefixURL"` BasePrefixURL string `yaml:"basePrefixURL"`
SuffixDirectoryURL string `yaml:"suffixDirectoryURL"` SuffixDirectoryURL string `yaml:"suffixDirectoryURL"`
SuffixFileURL string `yaml:"suffixFileURL"` SuffixFileURL string `yaml:"suffixFileURL"`
RangeSupported bool `yaml:"rangeSupported"`
PathLengthLimit uint `yaml:"pathLengthLimit"` //The length of the path (Number of entries in the path) to return in the responses; (If 0: defaults to 1, if the username is not expected to be provided by the request, otherwise defaulting to 2)
SuffixImportURL string `yaml:"suffixImportURL"`
BasePrefixSourceURL string `yaml:"basePrefixSourceURL"`
CacheSettings CacheSettingsYaml `yaml:"cacheSettings"`
} }
func (zy ZoneYaml) GetPackageMetaTagOutputter() *outputMeta.PackageMetaTagOutputter { func (zy ZoneYaml) GetPackageMetaTagOutputter() *outputMeta.PackageMetaTagOutputter {
@ -20,11 +25,22 @@ func (zy ZoneYaml) GetPackageMetaTagOutputter() *outputMeta.PackageMetaTagOutput
if !zy.UsernameProvided { if !zy.UsernameProvided {
theUsername = zy.Username theUsername = zy.Username
} }
pthLength := zy.PathLengthLimit
if pthLength == 0 {
if zy.UsernameProvided {
pthLength = 2
} else {
pthLength = 1
}
}
return &outputMeta.PackageMetaTagOutputter{ return &outputMeta.PackageMetaTagOutputter{
BasePath: zy.BasePath, BasePath: zy.BasePath,
Username: theUsername, Username: theUsername,
BasePrefixURL: zy.BasePrefixURL, BasePrefixURL: zy.BasePrefixURL,
SuffixDirectoryURL: zy.SuffixDirectoryURL, SuffixDirectoryURL: zy.SuffixDirectoryURL,
SuffixFileURL: zy.SuffixFileURL, SuffixFileURL: zy.SuffixFileURL,
PathLengthLimit: pthLength,
SuffixImportURL: zy.SuffixImportURL,
BasePrefixSourceURL: zy.BasePrefixSourceURL,
} }
} }

View File

@ -12,7 +12,14 @@ zones: #An array of zones
havePageContents: true #Output a header and link to the target repo havePageContents: true #Output a header and link to the target repo
basePath: "localhost" #The base-path, also known as, package name basePath: "localhost" #The base-path, also known as, package name
basePrefixURL: "http://localhost" #The base git URL basePrefixURL: "http://localhost" #The base git URL
usernameProvided: true #If the username would be provided in requests to the server (When false the value of username can be used) usernameProvided: true #If the username is expected to be provided in requests to the server (When false the value of username can be used)
username: "captain-alm" #The username to append to the start of a path under the prefix username: "captain-alm" #The username to append to the start of a path under the prefix
suffixDirectoryURL: "src/branch/master{/dir}" #The suffix location of the main branch for directory usage suffixDirectoryURL: "src/branch/master{/dir}" #The suffix location of the main branch for directory usage
suffixFileURL: "src/branch/master{/dir}/{file}#L{line}" #The suffix location of the main branch for file usage suffixFileURL: "src/branch/master{/dir}/{file}#L{line}" #The suffix location of the main branch for file usage
rangeSupported: true #Are range requests supported
pathLengthLimit: 0 #The length of the returned paths in the responses (Number of path entries); (If 0: defaults to 1, if the username is not expected to be provided by the request, otherwise defaulting to 2)
cacheSettings: #Cache settings
maxAge: 0 #The maximum age of the cache
notModifiedUsingLastModified: true #Are the conditional headers attached to Last-Modified used to work out if to send a 304 Cache Redirect
notModifiedUsingETags: true #Are the conditional headers attached to ETag used to work out if to send a 304 Cache Redirect

15
gopkghsrv.service Normal file
View File

@ -0,0 +1,15 @@
# GO PKG Header Service
[Unit]
Description=GO PKG Header Service
[Service]
WorkingDirectory=/etc/gopkghsrv
ExecStart=/usr/local/bin/gopkghsrv
User=www-data
Group=www-data
Type=simple
Restart=on-failure
RestartSec=15
[Install]
WantedBy=multi-user.target

15
gopkghsrv_.service Normal file
View File

@ -0,0 +1,15 @@
# GO PKG Header Service (Dev)
[Unit]
Description=GO PKG Header Service (Dev)
[Service]
WorkingDirectory=/etc/gopkghsrv_
ExecStart=/usr/local/bin/gopkghsrv_
User=www-data
Group=www-data
Type=simple
Restart=on-failure
RestartSec=15
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,122 @@
package outputMeta
import (
"path"
"strings"
)
type PackageMetaTagOutputter struct {
BasePath string
Username string //If set, the outputter will do /{repo}/ for repos rather than /{user}/{repo}/
BasePrefixURL string
SuffixDirectoryURL string
SuffixFileURL string
PathLengthLimit uint //The number of path entries in the go import paths
SuffixImportURL string
BasePrefixSourceURL string //If blank, use BasePrefixURL instead
}
func (pkgMTO *PackageMetaTagOutputter) GetMetaTags(pathIn string) string {
return "<meta name=\"go-import\" content=\"" + pkgMTO.GetMetaContentForGoImport(pathIn) + "\">\r\n" +
"<meta name=\"go-source\" content=\"" + pkgMTO.GetMetaContentForGoSource(pathIn) + "\">"
}
func (pkgMTO *PackageMetaTagOutputter) GetMetaContentForGoImport(pathIn string) string {
pathLoc := pkgMTO.GetPath(pathIn)
return pkgMTO.getPrefix(pathLoc) + " git " + pkgMTO.getHomeURL(pathLoc, false) + pkgMTO.SuffixImportURL
}
func (pkgMTO *PackageMetaTagOutputter) GetMetaContentForGoSource(pathIn string) string {
pathLoc := pkgMTO.GetPath(pathIn)
return pkgMTO.getPrefix(pathLoc) + " " + pkgMTO.getHomeURL(pathLoc, true) + " " +
pkgMTO.getDirectoryURL(pathLoc) + " " + pkgMTO.getFileURL(pathLoc)
}
func (pkgMTO *PackageMetaTagOutputter) GetPath(pathIn string) string {
cleaned := path.Clean(pathIn)
if cleaned == "/" || cleaned == "." {
return cleaned
}
split := strings.Split(cleaned, "/")
toReturn := ""
for i := 1; i < len(split) && i < int(pkgMTO.PathLengthLimit)+1; i++ {
toReturn += split[i] + "/"
}
return toReturn[:len(toReturn)-1]
}
func (pkgMTO *PackageMetaTagOutputter) assureBasePrefixURL() (failed bool) {
if pkgMTO.BasePrefixURL == "" {
if pkgMTO.BasePath == "" {
return true
}
pkgMTO.BasePrefixURL = "http://" + pkgMTO.BasePath
}
return false
}
func (pkgMTO *PackageMetaTagOutputter) assureBasePrefixSourceURL() (failed bool) {
if pkgMTO.BasePrefixSourceURL == "" {
if pkgMTO.assureBasePrefixURL() {
return true
}
if pkgMTO.BasePrefixURL == "" {
return true
}
pkgMTO.BasePrefixSourceURL = pkgMTO.BasePrefixURL
}
return false
}
func (pkgMTO *PackageMetaTagOutputter) getPrefix(pathIn string) string {
if pkgMTO.BasePath == "" {
return "_"
}
return path.Join(pkgMTO.BasePath, pathIn)
}
func (pkgMTO *PackageMetaTagOutputter) getHomeURL(pathIn string, isSource bool) string {
bpURL := ""
if isSource {
if pkgMTO.assureBasePrefixSourceURL() {
return "_"
} else {
bpURL = pkgMTO.BasePrefixSourceURL
}
} else {
if pkgMTO.assureBasePrefixURL() {
return "_"
} else {
bpURL = pkgMTO.BasePrefixURL
}
}
if pkgMTO.Username == "" {
return bpURL + "/" + strings.TrimLeft(path.Clean(pathIn), "/")
} else {
return bpURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn), "/")
}
}
func (pkgMTO *PackageMetaTagOutputter) getDirectoryURL(pathIn string) string {
if pkgMTO.assureBasePrefixSourceURL() || pkgMTO.SuffixDirectoryURL == "" {
return "_"
}
if pkgMTO.Username == "" {
return pkgMTO.BasePrefixSourceURL + "/" + strings.TrimLeft(path.Join(pathIn, pkgMTO.SuffixDirectoryURL), "/")
} else {
return pkgMTO.BasePrefixSourceURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn, pkgMTO.SuffixDirectoryURL), "/")
}
}
func (pkgMTO *PackageMetaTagOutputter) getFileURL(pathIn string) string {
if pkgMTO.assureBasePrefixSourceURL() || pkgMTO.SuffixFileURL == "" {
return "_"
}
if pkgMTO.Username == "" {
return pkgMTO.BasePrefixSourceURL + "/" + strings.TrimLeft(path.Join(pathIn, pkgMTO.SuffixFileURL), "/")
} else {
return pkgMTO.BasePrefixSourceURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn, pkgMTO.SuffixFileURL), "/")
}
}

View File

@ -1,81 +0,0 @@
package outputMeta
import (
"path"
"strings"
)
type PackageMetaTagOutputter struct {
BasePath string
Username string //If set, the outputter will do /{repo}/ for repos rather than /{user}/{repo}/
BasePrefixURL string
SuffixDirectoryURL string
SuffixFileURL string
}
func (pkgMTO *PackageMetaTagOutputter) GetMetaTags(pathIn string) string {
return "<meta name=\"go-import\" content=\"" + pkgMTO.GetMetaContentForGoImport(pathIn) + "\">\r\n" +
"<meta name=\"go-source\" content=\"" + pkgMTO.GetMetaContentForGoSource(pathIn) + "\">"
}
func (pkgMTO *PackageMetaTagOutputter) GetMetaContentForGoImport(pathIn string) string {
return pkgMTO.getPrefix(pathIn) + " git " + pkgMTO.getHomeURL(pathIn)
}
func (pkgMTO *PackageMetaTagOutputter) GetMetaContentForGoSource(pathIn string) string {
return pkgMTO.getPrefix(pathIn) + " " + pkgMTO.getHomeURL(pathIn) + " " +
pkgMTO.getDirectoryURL(pathIn) + " " + pkgMTO.getFileURL(pathIn)
}
func (pkgMTO *PackageMetaTagOutputter) assureBasePrefixURL() (failed bool) {
if pkgMTO.BasePrefixURL == "" {
if pkgMTO.BasePath == "" {
return true
}
pkgMTO.BasePrefixURL = "http://" + pkgMTO.BasePath
}
return false
}
func (pkgMTO *PackageMetaTagOutputter) getPrefix(pathIn string) string {
if pkgMTO.BasePath == "" {
return "_"
}
return path.Join(pkgMTO.BasePath, pathIn)
}
func (pkgMTO *PackageMetaTagOutputter) getHomeURL(pathIn string) string {
if pkgMTO.assureBasePrefixURL() {
return "_"
}
if pkgMTO.Username == "" {
return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Clean(pathIn), "/")
} else {
return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn), "/")
}
}
func (pkgMTO *PackageMetaTagOutputter) getDirectoryURL(pathIn string) string {
if pkgMTO.assureBasePrefixURL() || pkgMTO.SuffixDirectoryURL == "" {
return "_"
}
if pkgMTO.Username == "" {
return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pathIn, pkgMTO.SuffixDirectoryURL), "/")
} else {
return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn, pkgMTO.SuffixDirectoryURL), "/")
}
}
func (pkgMTO *PackageMetaTagOutputter) getFileURL(pathIn string) string {
if pkgMTO.assureBasePrefixURL() || pkgMTO.SuffixFileURL == "" {
return "_"
}
if pkgMTO.Username == "" {
return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pathIn, pkgMTO.SuffixFileURL), "/")
} else {
return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn, pkgMTO.SuffixFileURL), "/")
}
}

View File

@ -20,8 +20,8 @@ func (htm handlerTemplateMarshal) GetGoSourceMetaContent() string {
func (htm handlerTemplateMarshal) GetLink() string { func (htm handlerTemplateMarshal) GetLink() string {
if htm.PageHandler.MetaOutput.Username == "" { if htm.PageHandler.MetaOutput.Username == "" {
return htm.PageHandler.MetaOutput.BasePrefixURL + "/" + strings.TrimLeft(path.Clean(htm.RequestPath), "/") return htm.PageHandler.MetaOutput.BasePrefixSourceURL + "/" + strings.TrimLeft(path.Clean(htm.PageHandler.MetaOutput.GetPath(htm.RequestPath)), "/")
} else { } else {
return htm.PageHandler.MetaOutput.BasePrefixURL + "/" + strings.TrimLeft(path.Join(htm.PageHandler.MetaOutput.Username, htm.RequestPath), "/") return htm.PageHandler.MetaOutput.BasePrefixSourceURL + "/" + strings.TrimLeft(path.Join(htm.PageHandler.MetaOutput.Username, htm.PageHandler.MetaOutput.GetPath(htm.RequestPath)), "/")
} }
} }

97
web/page-handler.go Normal file
View File

@ -0,0 +1,97 @@
package web
import (
_ "embed"
"golang.captainalm.com/GOPackageHeaderServer/conf"
"golang.captainalm.com/GOPackageHeaderServer/outputMeta"
"golang.captainalm.com/GOPackageHeaderServer/web/utils"
"html/template"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"strconv"
"time"
)
type PageHandler struct {
Name string
CSS string
OutputPage bool
RangeSupported bool
CacheSettings conf.CacheSettingsYaml
MetaOutput *outputMeta.PackageMetaTagOutputter
}
var startTime = time.Now()
//go:embed output-page.html
var outputPage string
var pageTemplateFuncMap = template.FuncMap{
"isNotEmpty": func(stringIn string) bool {
return stringIn != ""
},
}
func (pgh *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodGet || request.Method == http.MethodHead {
tmpl, err := template.New("page-handler").Funcs(pageTemplateFuncMap).Parse(outputPage)
if err != nil {
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusInternalServerError, "Page Template Parsing Failure")
return
}
tm := handlerTemplateMarshal{
PageHandler: *pgh,
RequestPath: request.URL.Path,
}
theBuffer := &utils.BufferedWriter{}
err = tmpl.Execute(theBuffer, tm)
if err != nil {
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusInternalServerError, "Page Template Execution Failure")
return
}
writer.Header().Set("Content-Length", strconv.Itoa(len(theBuffer.Data)))
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
utils.SetLastModifiedHeader(writer.Header(), startTime)
utils.SetCacheHeaderWithAge(writer.Header(), pgh.CacheSettings.MaxAge, startTime)
theETag := utils.GetValueForETagUsingBufferedWriter(theBuffer)
writer.Header().Set("ETag", theETag)
if utils.ProcessSupportedPreconditionsForNext(writer, request, startTime, theETag, pgh.CacheSettings.NotModifiedResponseUsingLastModified, pgh.CacheSettings.NotModifiedResponseUsingETags) {
httpRangeParts := utils.ProcessRangePreconditions(int64(len(theBuffer.Data)), writer, request, startTime, theETag, pgh.RangeSupported)
if httpRangeParts != nil {
if len(httpRangeParts) <= 1 {
var theWriter io.Writer = writer
if len(httpRangeParts) == 1 {
theWriter = utils.NewPartialRangeWriter(theWriter, httpRangeParts[0])
}
_, _ = theWriter.Write(theBuffer.Data)
} else {
multWriter := multipart.NewWriter(writer)
writer.Header().Set("Content-Type", "multipart/byteranges; boundary="+multWriter.Boundary())
for _, currentPart := range httpRangeParts {
mimePart, err := multWriter.CreatePart(textproto.MIMEHeader{
"Content-Range": {currentPart.ToField(int64(len(theBuffer.Data)))},
"Content-Type": {"text/plain; charset=utf-8"},
})
if err != nil {
break
}
_, err = mimePart.Write(theBuffer.Data[currentPart.Start : currentPart.Start+currentPart.Length])
if err != nil {
break
}
}
_ = multWriter.Close()
}
}
}
} else {
writer.Header().Set("Allow", http.MethodOptions+", "+http.MethodGet+", "+http.MethodHead)
if request.Method == http.MethodOptions {
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "")
} else {
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusMethodNotAllowed, "")
}
}
}

View File

@ -1,57 +0,0 @@
package web
import (
_ "embed"
"golang.captainalm.com/GOPackageHeaderServer/outputMeta"
"html/template"
"net/http"
"strconv"
)
type PageHandler struct {
Name string
CSS string
OutputPage bool
MetaOutput *outputMeta.PackageMetaTagOutputter
}
//go:embed outputpage.html
var outputPage string
var pageTemplateFuncMap template.FuncMap = template.FuncMap{
"isNotEmpty": func(stringIn string) bool {
return stringIn != ""
},
}
func (pgh *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodGet || request.Method == http.MethodHead {
tmpl, err := template.New("page-handler").Funcs(pageTemplateFuncMap).Parse(outputPage)
if err != nil {
writeResponseHeaderCanWriteBody(request.Method, writer, http.StatusInternalServerError, "Page Template Parsing Failure")
return
}
tm := handlerTemplateMarshal{
PageHandler: *pgh,
RequestPath: request.URL.Path,
}
theBuffer := &BufferedWriter{}
err = tmpl.Execute(theBuffer, tm)
if err != nil {
writeResponseHeaderCanWriteBody(request.Method, writer, http.StatusInternalServerError, "Page Template Execution Failure")
return
}
writer.Header().Set("Content-Length", strconv.Itoa(len(theBuffer.Data)))
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
if writeResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "") {
_, _ = writer.Write(theBuffer.Data)
}
} else {
writer.Header().Set("Allow", http.MethodOptions+", "+http.MethodGet+", "+http.MethodHead)
if request.Method == http.MethodOptions {
writeResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "")
} else {
writeResponseHeaderCanWriteBody(request.Method, writer, http.StatusMethodNotAllowed, "")
}
}
}

View File

@ -1,33 +0,0 @@
package web
import (
"net/http"
"strconv"
)
func writeResponseHeaderCanWriteBody(method string, rw http.ResponseWriter, statusCode int, message string) bool {
hasBody := method != http.MethodHead && method != http.MethodOptions
if hasBody && message != "" {
rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
rw.Header().Set("X-Content-Type-Options", "nosniff")
rw.Header().Set("Content-Length", strconv.Itoa(len(message)+2))
}
rw.WriteHeader(statusCode)
if hasBody {
if message != "" {
_, _ = rw.Write([]byte(message + "\r\n"))
return false
}
return true
}
return false
}
type BufferedWriter struct {
Data []byte
}
func (c *BufferedWriter) Write(p []byte) (n int, err error) {
c.Data = append(c.Data, p...)
return len(p), nil
}

View File

@ -0,0 +1,23 @@
package utils
import (
"crypto"
"encoding/hex"
)
type BufferedWriter struct {
Data []byte
}
func (c *BufferedWriter) Write(p []byte) (n int, err error) {
c.Data = append(c.Data, p...)
return len(p), nil
}
func (c *BufferedWriter) GetHashString() string {
theHash := crypto.SHA1.New()
_, _ = theHash.Write(c.Data)
theSum := theHash.Sum(nil)
theHash.Reset()
return hex.EncodeToString(theSum)
}

View File

@ -0,0 +1,78 @@
package utils
import (
"strconv"
"strings"
)
type ContentRangeValue struct {
Start, Length int64
}
func (rstrc ContentRangeValue) ToField(maxLength int64) string {
return "bytes " + strconv.FormatInt(rstrc.Start, 10) + "-" + strconv.FormatInt(rstrc.Start+rstrc.Length-1, 10) + "/" + strconv.FormatInt(maxLength, 10)
}
func GetRanges(rangeStringIn string, maxLength int64) []ContentRangeValue {
actualRangeString := strings.TrimPrefix(rangeStringIn, "bytes=")
if strings.ContainsAny(actualRangeString, ",") {
seperated := strings.Split(actualRangeString, ",")
toReturn := make([]ContentRangeValue, len(seperated))
pos := 0
for _, s := range seperated {
if cRange, ok := GetRange(s, maxLength); ok {
toReturn[pos] = cRange
pos += 1
}
}
if pos == 0 {
return nil
}
return toReturn[:pos]
}
if cRange, ok := GetRange(actualRangeString, maxLength); ok {
return []ContentRangeValue{cRange}
}
return nil
}
func GetRange(rangePartIn string, maxLength int64) (ContentRangeValue, bool) {
before, after, done := strings.Cut(rangePartIn, "-")
before = strings.Trim(before, " ")
after = strings.Trim(after, " ")
if !done {
return ContentRangeValue{}, false
}
var parsedAfter, parsedBefore int64 = -1, -1
if after != "" {
if parsed, err := strconv.ParseInt(after, 10, 64); err == nil {
parsedAfter = parsed
} else {
return ContentRangeValue{}, false
}
}
if before != "" {
if parsed, err := strconv.ParseInt(before, 10, 64); err == nil {
parsedBefore = parsed
} else {
return ContentRangeValue{}, false
}
}
if parsedBefore >= 0 && parsedAfter > parsedBefore && parsedAfter < maxLength {
return ContentRangeValue{
Start: parsedBefore,
Length: parsedAfter - parsedBefore + 1,
}, true
} else if parsedAfter < 0 && parsedBefore >= 0 && parsedBefore < maxLength {
return ContentRangeValue{
Start: parsedBefore,
Length: maxLength - parsedBefore,
}, true
} else if parsedBefore < 0 && parsedAfter >= 1 && maxLength-parsedAfter >= 0 {
return ContentRangeValue{
Start: maxLength - parsedAfter,
Length: parsedAfter,
}, true
}
return ContentRangeValue{}, false
}

View File

@ -0,0 +1,10 @@
package utils
type CountingWriter struct {
Length int64
}
func (c *CountingWriter) Write(p []byte) (n int, err error) {
c.Length += int64(len(p))
return len(p), nil
}

42
web/utils/etag.go Normal file
View File

@ -0,0 +1,42 @@
package utils
import (
"strings"
)
func GetValueForETagUsingBufferedWriter(bWriter *BufferedWriter) string {
return "\"" + bWriter.GetHashString() + "\""
}
func GetETagValues(stringIn string) []string {
if strings.ContainsAny(stringIn, ",") {
seperated := strings.Split(stringIn, ",")
toReturn := make([]string, len(seperated))
pos := 0
for _, s := range seperated {
cETag := GetETagValue(s)
if cETag != "" {
toReturn[pos] = cETag
pos += 1
}
}
if pos == 0 {
return nil
}
return toReturn[:pos]
}
toReturn := []string{GetETagValue(stringIn)}
if toReturn[0] == "" {
return nil
}
return toReturn
}
func GetETagValue(stringIn string) string {
startIndex := strings.IndexAny(stringIn, "\"") + 1
endIndex := strings.LastIndexAny(stringIn, "\"")
if endIndex > startIndex {
return stringIn[startIndex:endIndex]
}
return ""
}

View File

@ -0,0 +1,47 @@
package utils
import "io"
func NewPartialRangeWriter(writerIn io.Writer, httpRangeIn ContentRangeValue) io.Writer {
return &PartialRangeWriter{
passedWriter: writerIn,
passedWriterIndex: 0,
httpRange: httpRangeIn,
exclusiveLastIndex: httpRangeIn.Start + httpRangeIn.Length,
}
}
type PartialRangeWriter struct {
passedWriter io.Writer
passedWriterIndex int64
exclusiveLastIndex int64
httpRange ContentRangeValue
}
func (prw *PartialRangeWriter) Write(p []byte) (n int, err error) {
var pOffsetIndex int64 = -1
if prw.passedWriterIndex >= prw.httpRange.Start && prw.passedWriterIndex < prw.exclusiveLastIndex {
pOffsetIndex = 0
} else if prw.passedWriterIndex+int64(len(p)) > prw.httpRange.Start && prw.passedWriterIndex < prw.exclusiveLastIndex {
pOffsetIndex = prw.httpRange.Start - prw.passedWriterIndex
prw.passedWriterIndex += pOffsetIndex
} else {
prw.passedWriterIndex += int64(len(p))
}
if pOffsetIndex >= 0 {
if prw.passedWriterIndex+(int64(len(p))-pOffsetIndex) <= prw.exclusiveLastIndex {
written, err := prw.passedWriter.Write(p[pOffsetIndex:])
prw.passedWriterIndex += int64(written)
if err != nil {
return written, err
}
} else {
written, err := prw.passedWriter.Write(p[pOffsetIndex : prw.exclusiveLastIndex-prw.passedWriterIndex+pOffsetIndex])
prw.passedWriterIndex += int64(written)
if err != nil {
return written, err
}
}
}
return n, nil
}

View File

@ -0,0 +1,152 @@
package utils
import (
"mime/multipart"
"net/http"
"net/textproto"
"strconv"
"strings"
"time"
)
func ProcessSupportedPreconditionsForNext(rw http.ResponseWriter, req *http.Request, modT time.Time, etag string, noBypassModify bool, noBypassMatch bool) bool {
theStrippedETag := GetETagValue(etag)
if noBypassMatch && theStrippedETag != "" && req.Header.Get("If-None-Match") != "" {
etagVals := GetETagValues(req.Header.Get("If-None-Match"))
conditionSuccess := false
for _, s := range etagVals {
if s == theStrippedETag {
conditionSuccess = true
break
}
}
if conditionSuccess {
WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusNotModified, "")
return false
}
}
if noBypassMatch && theStrippedETag != "" && req.Header.Get("If-Match") != "" {
etagVals := GetETagValues(req.Header.Get("If-Match"))
conditionFailed := true
for _, s := range etagVals {
if s == theStrippedETag {
conditionFailed = false
break
}
}
if conditionFailed {
SwitchToNonCachingHeaders(rw.Header())
rw.Header().Del("Content-Type")
rw.Header().Del("Content-Length")
WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusPreconditionFailed, "")
return false
}
}
if noBypassModify && !modT.IsZero() && req.Header.Get("If-Modified-Since") != "" {
parse, err := time.Parse(http.TimeFormat, req.Header.Get("If-Modified-Since"))
if err == nil && modT.Before(parse) || strings.EqualFold(modT.Format(http.TimeFormat), req.Header.Get("If-Modified-Since")) {
WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusNotModified, "")
return false
}
}
if noBypassModify && !modT.IsZero() && req.Header.Get("If-Unmodified-Since") != "" {
parse, err := time.Parse(http.TimeFormat, req.Header.Get("If-Unmodified-Since"))
if err == nil && modT.After(parse) {
SwitchToNonCachingHeaders(rw.Header())
rw.Header().Del("Content-Type")
rw.Header().Del("Content-Length")
WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusPreconditionFailed, "")
return false
}
}
return true
}
func ProcessRangePreconditions(maxLength int64, rw http.ResponseWriter, req *http.Request, modT time.Time, etag string, supported bool) []ContentRangeValue {
canDoRange := supported
theStrippedETag := GetETagValue(etag)
modTStr := modT.Format(http.TimeFormat)
if canDoRange {
rw.Header().Set("Accept-Ranges", "bytes")
}
if canDoRange && !modT.IsZero() && strings.HasSuffix(req.Header.Get("If-Range"), "GMT") {
newModT, err := time.Parse(http.TimeFormat, modTStr)
parse, err := time.Parse(http.TimeFormat, req.Header.Get("If-Range"))
if err == nil && !newModT.Equal(parse) {
canDoRange = false
}
} else if canDoRange && theStrippedETag != "" && req.Header.Get("If-Range") != "" {
if GetETagValue(req.Header.Get("If-Range")) != theStrippedETag {
canDoRange = false
}
}
if canDoRange && strings.HasPrefix(req.Header.Get("Range"), "bytes=") {
if theRanges := GetRanges(req.Header.Get("Range"), maxLength); len(theRanges) != 0 {
if len(theRanges) == 1 {
rw.Header().Set("Content-Length", strconv.FormatInt(theRanges[0].Length, 10))
rw.Header().Set("Content-Range", theRanges[0].ToField(maxLength))
} else {
theSize := GetMultipartLength(theRanges, rw.Header().Get("Content-Type"), maxLength)
rw.Header().Set("Content-Length", strconv.FormatInt(theSize, 10))
}
if WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusPartialContent, "") {
return theRanges
} else {
return nil
}
} else {
SwitchToNonCachingHeaders(rw.Header())
rw.Header().Del("Content-Type")
rw.Header().Del("Content-Length")
rw.Header().Set("Content-Range", "bytes */"+strconv.FormatInt(maxLength, 10))
WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusRequestedRangeNotSatisfiable, "")
return nil
}
}
if WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusOK, "") {
return make([]ContentRangeValue, 0)
}
return nil
}
func GetMultipartLength(parts []ContentRangeValue, contentType string, maxLength int64) int64 {
cWriter := &CountingWriter{Length: 0}
var returnLength int64 = 0
multWriter := multipart.NewWriter(cWriter)
for _, currentPart := range parts {
_, _ = multWriter.CreatePart(textproto.MIMEHeader{
"Content-Range": {currentPart.ToField(maxLength)},
"Content-Type": {contentType},
})
returnLength += currentPart.Length
}
_ = multWriter.Close()
returnLength += cWriter.Length
return returnLength
}
func WriteResponseHeaderCanWriteBody(method string, rw http.ResponseWriter, statusCode int, message string) bool {
hasBody := method != http.MethodHead && method != http.MethodOptions
if hasBody && message != "" {
rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
rw.Header().Set("X-Content-Type-Options", "nosniff")
rw.Header().Set("Content-Length", strconv.Itoa(len(message)+2))
SetNeverCacheHeader(rw.Header())
}
rw.WriteHeader(statusCode)
if hasBody {
if message != "" {
_, _ = rw.Write([]byte(message + "\r\n"))
return false
}
return true
}
return false
}

45
web/utils/utils.go Normal file
View File

@ -0,0 +1,45 @@
package utils
import (
"net/http"
"strconv"
"time"
)
func SetNeverCacheHeader(header http.Header) {
header.Set("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate")
header.Set("Pragma", "no-cache")
}
func SetLastModifiedHeader(header http.Header, modTime time.Time) {
if !modTime.IsZero() {
header.Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
}
}
func SetCacheHeaderWithAge(header http.Header, maxAge uint, modifiedTime time.Time) {
header.Set("Cache-Control", "max-age="+strconv.Itoa(int(maxAge))+", must-revalidate")
if maxAge > 0 {
checkerSecondsBetween := int64(time.Now().UTC().Sub(modifiedTime.UTC()).Seconds())
if checkerSecondsBetween < 0 {
checkerSecondsBetween *= -1
}
header.Set("Age", strconv.FormatUint(uint64(checkerSecondsBetween)%uint64(maxAge), 10))
}
}
func SwitchToNonCachingHeaders(header http.Header) {
SetNeverCacheHeader(header)
if header.Get("Last-Modified") != "" {
header.Del("Last-Modified")
}
if header.Get("Age") != "" {
header.Del("Age")
}
if header.Get("Expires") != "" {
header.Del("Expires")
}
if header.Get("ETag") != "" {
header.Del("ETag")
}
}

View File

@ -3,6 +3,7 @@ package web
import ( import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"golang.captainalm.com/GOPackageHeaderServer/conf" "golang.captainalm.com/GOPackageHeaderServer/conf"
"golang.captainalm.com/GOPackageHeaderServer/web/utils"
"log" "log"
"net/http" "net/http"
"strings" "strings"
@ -13,10 +14,12 @@ func New(yaml conf.ConfigYaml) (*http.Server, map[string]*PageHandler) {
var pages = make(map[string]*PageHandler) var pages = make(map[string]*PageHandler)
for _, zc := range yaml.Zones { for _, zc := range yaml.Zones {
currentPage := &PageHandler{ currentPage := &PageHandler{
Name: zc.Name, Name: zc.Name,
CSS: zc.CssURL, CSS: zc.CssURL,
OutputPage: zc.HavePageContents, OutputPage: zc.HavePageContents,
MetaOutput: zc.GetPackageMetaTagOutputter(), RangeSupported: zc.RangeSupported,
MetaOutput: zc.GetPackageMetaTagOutputter(),
CacheSettings: zc.CacheSettings,
} }
for _, d := range zc.Domains { for _, d := range zc.Domains {
ld := strings.ToLower(d) ld := strings.ToLower(d)
@ -56,13 +59,13 @@ func runBackgroundHttp(s *http.Server) {
func domainNotAllowed(rw http.ResponseWriter, req *http.Request) { func domainNotAllowed(rw http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodGet || req.Method == http.MethodHead { if req.Method == http.MethodGet || req.Method == http.MethodHead {
writeResponseHeaderCanWriteBody(req.Method, rw, http.StatusNotFound, "Domain Not Allowed") utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusNotFound, "Domain Not Allowed")
} else { } else {
rw.Header().Set("Allow", http.MethodOptions+", "+http.MethodGet+", "+http.MethodHead) rw.Header().Set("Allow", http.MethodOptions+", "+http.MethodGet+", "+http.MethodHead)
if req.Method == http.MethodOptions { if req.Method == http.MethodOptions {
writeResponseHeaderCanWriteBody(req.Method, rw, http.StatusOK, "") utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusOK, "")
} else { } else {
writeResponseHeaderCanWriteBody(req.Method, rw, http.StatusMethodNotAllowed, "") utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusMethodNotAllowed, "")
} }
} }
} }