@ -14,6 +14,7 @@ import (
@@ -14,6 +14,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
)
@ -51,15 +52,14 @@ var slashesRegexp = regexp.MustCompile(`[^\\]\/`)
@@ -51,15 +52,14 @@ var slashesRegexp = regexp.MustCompile(`[^\\]\/`)
var newLine = "\r\n"
func writeHeader ( c net . Conn , code int , meta string ) {
fmt . Fprintf ( c , "%d %s%s" , code , meta , newLine )
var logLock sync . Mutex
if verbose {
log . Printf ( "< %d %s\n " , code , meta )
}
func writeHeader ( c net . Conn , code int , meta string ) int {
fmt . Fprintf ( c , "%d %s%s ", code , meta , newLine )
return code
}
func writeStatus ( c net . Conn , code int ) {
func writeStatus ( c net . Conn , code int ) int {
var meta string
switch code {
case statusTemporaryFailure :
@ -74,9 +74,10 @@ func writeStatus(c net.Conn, code int) {
@@ -74,9 +74,10 @@ func writeStatus(c net.Conn, code int) {
meta = "Proxy request refused"
}
writeHeader ( c , code , meta )
return code
}
func writeSuccess ( c net . Conn , serve * pathConfig , contentType string , size int64 ) {
func writeSuccess ( c net . Conn , serve * pathConfig , contentType string , size int64 ) int {
meta := contentType
if serve . Type != "" {
meta = serve . Type
@ -91,6 +92,7 @@ func writeSuccess(c net.Conn, serve *pathConfig, contentType string, size int64)
@@ -91,6 +92,7 @@ func writeSuccess(c net.Conn, serve *pathConfig, contentType string, size int64)
}
writeHeader ( c , statusSuccess , meta )
return statusSuccess
}
func scanCRLF ( data [ ] byte , atEOF bool ) ( advance int , token [ ] byte , err error ) {
@ -123,7 +125,7 @@ func replaceWithUserInput(command []string, request *url.URL) []string {
@@ -123,7 +125,7 @@ func replaceWithUserInput(command []string, request *url.URL) []string {
return newCommand
}
func servePath ( c * tls . Conn , request * url . URL , serve * pathConfig ) {
func servePath ( c * tls . Conn , request * url . URL , serve * pathConfig ) ( int , int64 ) {
resolvedPath := request . Path
requestSplit := strings . Split ( request . Path , "/" )
pathSlashes := len ( slashesRegexp . FindAllStringIndex ( serve . Path , - 1 ) )
@ -142,8 +144,7 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
@@ -142,8 +144,7 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
if ! serve . HiddenFiles {
for _ , piece := range requestSplit {
if len ( piece ) > 0 && piece [ 0 ] == '.' {
writeStatus ( c , statusTemporaryFailure )
return
return writeStatus ( c , statusTemporaryFailure ) , - 1
}
}
}
@ -159,8 +160,7 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
@@ -159,8 +160,7 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
for i := range requestSplit [ pathSlashes : ] {
info , err := os . Lstat ( path . Join ( root , strings . Join ( requestSplit [ pathSlashes : pathSlashes + i + 1 ] , "/" ) ) )
if err != nil || info . Mode ( ) & os . ModeSymlink == os . ModeSymlink {
writeStatus ( c , statusTemporaryFailure )
return
return writeStatus ( c , statusTemporaryFailure ) , - 1
}
}
}
@ -169,12 +169,10 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
@@ -169,12 +169,10 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
}
if serve . Proxy != "" {
serveProxy ( c , request , serve . Proxy )
return
return serveProxy ( c , request , serve . Proxy ) , - 1
} else if serve . FastCGI != "" {
if filePath == "" {
writeStatus ( c , statusNotFound )
return
return writeStatus ( c , statusNotFound ) , - 1
}
contentType := geminiType
@ -184,37 +182,32 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
@@ -184,37 +182,32 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
writeSuccess ( c , serve , contentType , - 1 )
serveFastCGI ( c , config . fcgiPools [ serve . FastCGI ] , request , filePath )
return
return statusSuccess , - 1
} else if serve . cmd != nil {
requireInput := serve . Input != "" || serve . SensitiveInput != ""
if requireInput {
newCommand := replaceWithUserInput ( serve . cmd , request )
if newCommand != nil {
serveCommand ( c , serve , request , newCommand )
return
return serveCommand ( c , serve , request , newCommand )
}
}
serveCommand ( c , serve , request , serve . cmd )
return
return serveCommand ( c , serve , request , serve . cmd )
}
if filePath == "" {
writeStatus ( c , statusNotFound )
return
return writeStatus ( c , statusNotFound ) , - 1
}
fi , err := os . Stat ( filePath )
if err != nil {
writeStatus ( c , statusNotFound )
return
return writeStatus ( c , statusNotFound ) , - 1
}
mode := fi . Mode ( )
hasTrailingSlash := len ( request . Path ) > 0 && request . Path [ len ( request . Path ) - 1 ] == '/'
if mode . IsDir ( ) {
if ! hasTrailingSlash {
writeHeader ( c , statusRedirectPermanent , request . String ( ) + "/" )
return
return writeHeader ( c , statusRedirectPermanent , request . String ( ) + "/" ) , - 1
}
_ , err := os . Stat ( path . Join ( filePath , "index.gmi" ) )
@ -222,11 +215,9 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
@@ -222,11 +215,9 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
_ , err := os . Stat ( path . Join ( filePath , "index.gemini" ) )
if err != nil {
if serve . ListDirectory {
serveDirList ( c , serve , request , filePath )
return
return serveDirList ( c , serve , request , filePath ) , - 1
}
writeStatus ( c , statusNotFound )
return
return writeStatus ( c , statusNotFound ) , - 1
}
filePath = path . Join ( filePath , "index.gemini" )
} else {
@ -234,14 +225,105 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
@@ -234,14 +225,105 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
}
} else if hasTrailingSlash && len ( request . Path ) > 1 {
r := request . String ( )
writeHeader ( c , statusRedirectPermanent , r [ : len ( r ) - 1 ] )
return
return writeHeader ( c , statusRedirectPermanent , r [ : len ( r ) - 1 ] ) , - 1
}
serveFile ( c , serve , filePath )
return statusSuccess , fi . Size ( )
}
func handleRequest ( c * tls . Conn , request * url . URL , requestData string ) ( int , int64 , string ) {
if request . Path == "" {
// Redirect to /
return writeHeader ( c , statusRedirectPermanent , requestData + "/" ) , - 1 , ""
}
pathBytes := [ ] byte ( request . Path )
strippedPath := request . Path
if strippedPath [ 0 ] == '/' {
strippedPath = strippedPath [ 1 : ]
}
var matchedHost bool
requestHostname := request . Hostname ( )
for hostname := range config . Hosts {
if requestHostname != hostname {
continue
}
matchedHost = true
for _ , serve := range config . Hosts [ hostname ] . Paths {
matchedRegexp := serve . r != nil && serve . r . Match ( pathBytes )
matchedPrefix := serve . r == nil && strings . HasPrefix ( request . Path , serve . Path )
if ! matchedRegexp && ! matchedPrefix {
continue
}
requireInput := serve . Input != "" || serve . SensitiveInput != ""
if request . RawQuery == "" && requireInput {
if serve . Input != "" {
return writeHeader ( c , statusInput , serve . Input ) , - 1 , ""
} else if serve . SensitiveInput != "" {
return writeHeader ( c , statusSensitiveInput , serve . SensitiveInput ) , - 1 , ""
}
}
if matchedRegexp || matchedPrefix {
status , size := servePath ( c , request , serve )
return status , size , serve . Log
}
}
break
}
if matchedHost {
return writeStatus ( c , statusNotFound ) , - 1 , ""
}
return writeStatus ( c , statusProxyRequestRefused ) , - 1 , ""
}
func serveConn ( c * tls . Conn ) {
func handleConn ( c * tls . Conn ) {
t := time . Now ( )
var request * url . URL
var logPath string
status := 0
size := int64 ( - 1 )
defer func ( ) {
if ! verbose && logPath == "" {
return
}
entry := logEntry ( request , status , size , time . Since ( t ) )
if verbose {
log . Println ( string ( entry ) )
}
if logPath == "" {
return
}
logLock . Lock ( )
defer logLock . Unlock ( )
f , err := os . OpenFile ( logPath , os . O_APPEND | os . O_WRONLY | os . O_CREATE , 0600 )
if err != nil {
log . Printf ( "ERROR: Failed to open log file at %s: %s" , logPath , err )
return
}
defer f . Close ( )
if _ , err = f . Write ( entry ) ; err != nil {
log . Printf ( "ERROR: Failed to write to log file at %s: %s" , logPath , err )
return
}
f . Write ( [ ] byte ( "\n" ) )
} ( )
defer c . Close ( )
c . SetReadDeadline ( time . Now ( ) . Add ( readTimeout ) )
var requestData string
scanner := bufio . NewScanner ( c )
if ! config . SaneEOL {
@ -251,7 +333,7 @@ func serveConn(c *tls.Conn) {
@@ -251,7 +333,7 @@ func serveConn(c *tls.Conn) {
requestData = scanner . Text ( )
}
if err := scanner . Err ( ) ; err != nil {
writeStatus ( c , statusBadRequest )
status = writeStatus ( c , statusBadRequest )
return
}
@ -266,24 +348,21 @@ func serveConn(c *tls.Conn) {
@@ -266,24 +348,21 @@ func serveConn(c *tls.Conn) {
clientCertKeys = append ( clientCertKeys , pubKey )
}
if verbose {
log . Printf ( "> %s\n" , requestData )
}
if len ( requestData ) > urlMaxLength || ! utf8 . ValidString ( requestData ) {
writeStatus ( c , statusBadRequest )
status = writeStatus ( c , statusBadRequest )
return
}
request , err := url . Parse ( requestData )
var err error
request , err = url . Parse ( requestData )
if err != nil {
writeStatus ( c , statusBadRequest )
status = writeStatus ( c , statusBadRequest )
return
}
requestHostname := request . Hostname ( )
if requestHostname == "" || strings . ContainsRune ( requestHostname , ' ' ) {
writeStatus ( c , statusBadRequest )
status = writeStatus ( c , statusBadRequest )
return
}
@ -299,79 +378,41 @@ func serveConn(c *tls.Conn) {
@@ -299,79 +378,41 @@ func serveConn(c *tls.Conn) {
request . Scheme = "gemini"
}
if request . Scheme != "gemini" || ( requestPort > 0 && requestPort != config . port ) {
writeStatus ( c , statusProxyRequestRefused )
}
if request . Path == "" {
// Redirect to /
writeHeader ( c , statusRedirectPermanent , requestData + "/" )
status = writeStatus ( c , statusProxyRequestRefused )
return
}
pathBytes := [ ] byte ( request . Path )
strippedPath := request . Path
if strippedPath [ 0 ] == '/' {
strippedPath = strippedPath [ 1 : ]
}
var matchedHost bool
for hostname := range config . Hosts {
if requestHostname != hostname {
continue
}
matchedHost = true
for _ , serve := range config . Hosts [ hostname ] . Paths {
matchedRegexp := serve . r != nil && serve . r . Match ( pathBytes )
matchedPrefix := serve . r == nil && strings . HasPrefix ( request . Path , serve . Path )
if ! matchedRegexp && ! matchedPrefix {
continue
}
requireInput := serve . Input != "" || serve . SensitiveInput != ""
if request . RawQuery == "" && requireInput {
if serve . Input != "" {
writeHeader ( c , statusInput , serve . Input )
return
} else if serve . SensitiveInput != "" {
writeHeader ( c , statusSensitiveInput , serve . SensitiveInput )
return
}
}
status , size , logPath = handleRequest ( c , request , requestData )
}
if matchedRegexp || matchedPrefix {
servePath ( c , request , serve )
return
}
func logEntry ( request * url . URL , status int , size int64 , elapsed time . Duration ) [ ] byte {
hostFormatted := "-"
if request . Hostname ( ) != "" {
hostFormatted = request . Hostname ( )
if request . Port ( ) != "" {
hostFormatted += ":" + request . Port ( )
} else {
hostFormatted += ":1965"
}
break
}
if matchedHost {
writeStatus ( c , statusNotFound )
} else {
writeStatus ( c , statusProxyRequestRefused )
timeFormatted := time . Now ( ) . Format ( "02/Jan/2006 03:04:05" )
sizeFormatted := "-"
if size >= 0 {
sizeFormatted = strconv . FormatInt ( size , 10 )
}
}
func handleConn ( c * tls . Conn ) {
if verbose {
t := time . Now ( )
defer func ( ) {
d := time . Since ( t )
if d > time . Second {
d = d . Round ( time . Second )
} else {
d = d . Round ( time . Millisecond )
}
log . Printf ( "took %s" , d )
} ( )
}
return [ ] byte ( fmt . Sprintf ( ` %s - - - [%s] "GET %s Gemini" %d %s %.4f ` , hostFormatted , timeFormatted , request . Path , status , sizeFormatted , elapsed . Seconds ( ) ) )
}
defer c . Close ( )
c . SetReadDeadline ( time . Now ( ) . Add ( readTimeout ) )
func handleListener ( l net . Listener ) {
for {
conn , err := l . Accept ( )
if err != nil {
log . Fatal ( err )
}
serveConn ( c )
go handleConn ( conn . ( * tls . Conn ) )
}
}
func getCertificate ( info * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
@ -385,17 +426,6 @@ func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
@@ -385,17 +426,6 @@ func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil , nil
}
func handleListener ( l net . Listener ) {
for {
conn , err := l . Accept ( )
if err != nil {
log . Fatal ( err )
}
go handleConn ( conn . ( * tls . Conn ) )
}
}
func listen ( address string ) {
tlsConfig := & tls . Config {
ClientAuth : tls . RequestClientCert ,