Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
f5f1625533 | |||
2e11c55981 | |||
59b5d686d4 | |||
1a48e7815f | |||
3910d29fa2 | |||
976d356398 | |||
0d4036d05c | |||
8c67a34250 | |||
8759f57f1c | |||
61864ac508 |
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="SwUserDefinedSpecifications">
|
|
||||||
<option name="specTypeByUrl">
|
|
||||||
<map />
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
37
Makefile
37
Makefile
@ -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}"
|
||||||
|
@ -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).
|
@ -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
7
conf/cache.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
type CacheSettingsYaml struct {
|
||||||
|
MaxAge uint `yaml:"maxAge"`
|
||||||
|
NotModifiedResponseUsingLastModified bool `yaml:"notModifiedUsingLastModified"`
|
||||||
|
NotModifiedResponseUsingETags bool `yaml:"notModifiedUsingETags"`
|
||||||
|
}
|
18
conf/zone.go
18
conf/zone.go
@ -8,11 +8,16 @@ type ZoneYaml struct {
|
|||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 has been provided
|
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
15
gopkghsrv.service
Normal 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
15
gopkghsrv_.service
Normal 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
|
122
outputMeta/package-meta-tag-outputter.go
Normal file
122
outputMeta/package-meta-tag-outputter.go
Normal 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), "/")
|
||||||
|
}
|
||||||
|
}
|
@ -1,85 +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 "_"
|
|
||||||
}
|
|
||||||
if pkgMTO.Username == "" {
|
|
||||||
return path.Join(pkgMTO.BasePath, pathIn)
|
|
||||||
} else {
|
|
||||||
return path.Join(pkgMTO.BasePath, pkgMTO.Username, 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), "/")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
97
web/page-handler.go
Normal 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, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
33
web/utils.go
33
web/utils.go
@ -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
|
|
||||||
}
|
|
23
web/utils/buffered-writer.go
Normal file
23
web/utils/buffered-writer.go
Normal 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)
|
||||||
|
}
|
78
web/utils/content-range-value.go
Normal file
78
web/utils/content-range-value.go
Normal 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
|
||||||
|
}
|
10
web/utils/counting-writer.go
Normal file
10
web/utils/counting-writer.go
Normal 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
42
web/utils/etag.go
Normal 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 ""
|
||||||
|
}
|
47
web/utils/partial-range-writer.go
Normal file
47
web/utils/partial-range-writer.go
Normal 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
|
||||||
|
}
|
152
web/utils/process-preconditions.go
Normal file
152
web/utils/process-preconditions.go
Normal 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
45
web/utils/utils.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
17
web/web.go
17
web/web.go
@ -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"
|
||||||
@ -16,7 +17,9 @@ func New(yaml conf.ConfigYaml) (*http.Server, map[string]*PageHandler) {
|
|||||||
Name: zc.Name,
|
Name: zc.Name,
|
||||||
CSS: zc.CssURL,
|
CSS: zc.CssURL,
|
||||||
OutputPage: zc.HavePageContents,
|
OutputPage: zc.HavePageContents,
|
||||||
|
RangeSupported: zc.RangeSupported,
|
||||||
MetaOutput: zc.GetPackageMetaTagOutputter(),
|
MetaOutput: zc.GetPackageMetaTagOutputter(),
|
||||||
|
CacheSettings: zc.CacheSettings,
|
||||||
}
|
}
|
||||||
for _, d := range zc.Domains {
|
for _, d := range zc.Domains {
|
||||||
ld := strings.ToLower(d)
|
ld := strings.ToLower(d)
|
||||||
@ -26,6 +29,7 @@ func New(yaml conf.ConfigYaml) (*http.Server, map[string]*PageHandler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
router.PathPrefix("/").HandlerFunc(domainNotAllowed)
|
||||||
if yaml.Listen.Identify {
|
if yaml.Listen.Identify {
|
||||||
router.Use(headerMiddleware)
|
router.Use(headerMiddleware)
|
||||||
}
|
}
|
||||||
@ -53,6 +57,19 @@ func runBackgroundHttp(s *http.Server) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func domainNotAllowed(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method == http.MethodGet || req.Method == http.MethodHead {
|
||||||
|
utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusNotFound, "Domain Not Allowed")
|
||||||
|
} else {
|
||||||
|
rw.Header().Set("Allow", http.MethodOptions+", "+http.MethodGet+", "+http.MethodHead)
|
||||||
|
if req.Method == http.MethodOptions {
|
||||||
|
utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusOK, "")
|
||||||
|
} else {
|
||||||
|
utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusMethodNotAllowed, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func headerMiddleware(next http.Handler) http.Handler {
|
func headerMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Server", "Clerie Gilbert")
|
w.Header().Set("Server", "Clerie Gilbert")
|
||||||
|
Loading…
Reference in New Issue
Block a user