2020-04-08 18:03:12 +00:00
package main
import (
2020-04-10 15:39:43 +00:00
"context"
2020-04-08 18:03:12 +00:00
"flag"
"fmt"
"net/http"
"path/filepath"
"regexp"
"strings"
2020-04-10 15:39:43 +00:00
"time"
2020-04-08 18:03:12 +00:00
2020-04-10 15:39:43 +00:00
"code.hackerspace.pl/hscloud/go/mirko"
"code.hackerspace.pl/hscloud/go/pki"
2020-04-08 18:03:12 +00:00
"github.com/golang/glog"
2020-04-10 15:39:43 +00:00
"google.golang.org/grpc"
2020-04-08 18:03:12 +00:00
2020-04-10 15:39:43 +00:00
dvpb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
2020-04-08 18:03:12 +00:00
"code.hackerspace.pl/hscloud/devtools/hackdoc/config"
"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
)
var (
flagListen = "127.0.0.1:8080"
2020-04-10 15:39:43 +00:00
flagDocRoot = ""
flagDepotViewAddress = ""
2020-04-08 18:03:12 +00:00
flagHackdocURL = ""
flagGitwebDefaultBranch = "master"
rePagePath = regexp . MustCompile ( ` ^/([A-Za-z0-9_\-/\. ]*)$ ` )
)
func init ( ) {
flag . Set ( "logtostderr" , "true" )
}
func main ( ) {
2020-04-10 15:39:43 +00:00
flag . StringVar ( & flagListen , "pub_listen" , flagListen , "Address to listen on for HTTP traffic" )
flag . StringVar ( & flagDocRoot , "docroot" , flagDocRoot , "Path from which to serve documents. Either this or depotview must be set" )
flag . StringVar ( & flagDepotViewAddress , "depotview" , flagDepotViewAddress , "gRPC endpoint of depotview to serve from Git. Either this or docroot must be set" )
2020-04-08 18:03:12 +00:00
flag . StringVar ( & flagHackdocURL , "hackdoc_url" , flagHackdocURL , "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes" )
2020-04-10 15:39:43 +00:00
flag . StringVar ( & source . FlagGitwebURLPattern , "gitweb_url_pattern" , source . FlagGitwebURLPattern , "Pattern to sprintf to for URL for viewing a file in Git. First string is ref/rev, second is bare file path (sans //)" )
2020-04-08 18:03:12 +00:00
flag . StringVar ( & flagGitwebDefaultBranch , "gitweb_default_rev" , flagGitwebDefaultBranch , "Default Git rev to render/link to" )
flag . Parse ( )
if flagHackdocURL == "" {
flagHackdocURL = fmt . Sprintf ( "http://%s" , flagListen )
}
2020-04-10 15:39:43 +00:00
if flagDocRoot == "" && flagDepotViewAddress == "" {
glog . Errorf ( "Either -docroot or -depotview must be set" )
}
if flagDocRoot != "" && flagDepotViewAddress != "" {
glog . Errorf ( "Only one of -docroot or -depotview must be set" )
2020-04-08 18:03:12 +00:00
}
2020-04-10 15:39:43 +00:00
m := mirko . New ( )
if err := m . Listen ( ) ; err != nil {
glog . Exitf ( "Listen(): %v" , err )
2020-04-08 18:03:12 +00:00
}
2020-04-10 15:39:43 +00:00
var s * service
if flagDocRoot != "" {
path , err := filepath . Abs ( flagDocRoot )
if err != nil {
glog . Exitf ( "Could not dereference path %q: %w" , path , err )
}
glog . Infof ( "Starting in docroot mode for %q -> %q" , flagDocRoot , path )
2020-04-08 18:03:12 +00:00
2020-04-10 15:39:43 +00:00
s = & service {
source : source . NewSingleRefProvider ( source . NewLocal ( path ) ) ,
}
} else {
glog . Infof ( "Starting in depotview mode (server %q)" , flagDepotViewAddress )
conn , err := grpc . Dial ( flagDepotViewAddress , pki . WithClientHSPKI ( ) )
if err != nil {
glog . Exitf ( "grpc.Dial(%q): %v" , flagDepotViewAddress , err )
}
stub := dvpb . NewDepotViewClient ( conn )
s = & service {
source : source . NewDepotView ( stub ) ,
}
2020-04-08 18:03:12 +00:00
}
2020-04-10 15:39:43 +00:00
mux := http . NewServeMux ( )
mux . HandleFunc ( "/" , s . handler )
srv := & http . Server { Addr : flagListen , Handler : mux }
glog . Infof ( "Listening on %q..." , flagListen )
go func ( ) {
if err := srv . ListenAndServe ( ) ; err != nil {
glog . Error ( err )
}
} ( )
<- m . Done ( )
ctx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Second )
defer cancel ( )
srv . Shutdown ( ctx )
2020-04-08 18:03:12 +00:00
}
type service struct {
2020-04-10 15:39:43 +00:00
source source . SourceProvider
2020-04-08 18:03:12 +00:00
}
func ( s * service ) handler ( w http . ResponseWriter , r * http . Request ) {
if r . Method != "GET" && r . Method != "HEAD" {
w . WriteHeader ( http . StatusMethodNotAllowed )
fmt . Fprintf ( w , "method not allowed" )
return
}
2020-04-10 15:39:43 +00:00
ref := r . URL . Query ( ) . Get ( "ref" )
if ref == "" {
ref = flagGitwebDefaultBranch
}
ctx := r . Context ( )
source , err := s . source . Source ( ctx , ref )
switch {
case err != nil :
glog . Errorf ( "Source(%q): %v" , ref , err )
handle500 ( w , r )
return
case source == nil :
handle404 ( w , r )
return
2020-04-08 18:03:12 +00:00
}
path := r . URL . Path
if match := rePagePath . FindStringSubmatch ( path ) ; match != nil {
2020-04-10 15:39:43 +00:00
req := & request {
w : w ,
r : r ,
ctx : r . Context ( ) ,
ref : ref ,
source : source ,
}
req . handlePage ( match [ 1 ] )
2020-04-08 18:03:12 +00:00
return
}
handle404 ( w , r )
}
2020-04-10 15:39:43 +00:00
type request struct {
w http . ResponseWriter
r * http . Request
ctx context . Context
ref string
source source . Source
// rpath is the path requested by the client
rpath string
}
func ( r * request ) handle500 ( ) {
handle500 ( r . w , r . r )
}
func ( r * request ) handle404 ( ) {
handle404 ( r . w , r . r )
}
func ( r * request ) logRequest ( format string , args ... interface { } ) {
logRequest ( r . w , r . r , format , args ... )
2020-04-08 18:03:12 +00:00
}
func urlPathToDepotPath ( url string ) string {
// Sanitize request.
parts := strings . Split ( url , "/" )
for i , p := range parts {
// Allow last part to be "", ie, for a path to end in /
if p == "" {
if i != len ( parts ) - 1 {
return ""
}
}
// net/http sanitizes this anyway, but we better be sure.
if p == "." || p == ".." {
return ""
}
}
path := "//" + strings . Join ( parts , "/" )
return path
}
2020-04-10 15:39:43 +00:00
func ( r * request ) handlePageAuto ( dirpath string ) {
cfg , err := config . ForPath ( r . ctx , r . source , dirpath )
2020-04-08 18:03:12 +00:00
if err != nil {
glog . Errorf ( "could not get config for path %q: %w" , dirpath , err )
2020-04-10 15:39:43 +00:00
r . handle500 ( )
2020-04-08 18:03:12 +00:00
return
}
for _ , f := range cfg . DefaultIndex {
fpath := dirpath + f
2020-04-10 15:39:43 +00:00
file , err := r . source . IsFile ( r . ctx , fpath )
2020-04-08 18:03:12 +00:00
if err != nil {
glog . Errorf ( "IsFile(%q): %w" , fpath , err )
2020-04-10 15:39:43 +00:00
r . handle500 ( )
2020-04-08 18:03:12 +00:00
return
}
if file {
2021-03-06 20:44:56 +00:00
ref := r . ref
if ref == flagGitwebDefaultBranch {
ref = ""
}
path := "/" + fpath
if ref != "" {
path += "?ref=" + ref
}
http . Redirect ( r . w , r . r , path , 302 )
2020-04-08 18:03:12 +00:00
return
}
}
2020-04-10 15:39:43 +00:00
r . handle404 ( )
2020-04-08 18:03:12 +00:00
}
2020-04-10 15:39:43 +00:00
func ( r * request ) handlePage ( page string ) {
r . rpath = urlPathToDepotPath ( page )
2020-04-08 18:03:12 +00:00
2020-04-10 15:39:43 +00:00
if strings . HasSuffix ( r . rpath , "/" ) {
2020-04-08 18:03:12 +00:00
// Directory path given, autoresolve.
2020-04-10 15:39:43 +00:00
dirpath := r . rpath
if r . rpath != "//" {
dirpath = strings . TrimSuffix ( r . rpath , "/" ) + "/"
2020-04-08 18:03:12 +00:00
}
2020-04-10 15:39:43 +00:00
r . handlePageAuto ( dirpath )
2020-04-08 18:03:12 +00:00
return
}
// Otherwise, try loading the file.
2020-04-10 15:39:43 +00:00
file , err := r . source . IsFile ( r . ctx , r . rpath )
2020-04-08 18:03:12 +00:00
if err != nil {
2020-04-10 15:39:43 +00:00
glog . Errorf ( "IsFile(%q): %w" , r . rpath , err )
r . handle500 ( )
2020-04-08 18:03:12 +00:00
return
}
// File exists, render that.
if file {
2020-04-10 15:39:43 +00:00
parts := strings . Split ( r . rpath , "/" )
2020-04-08 18:03:12 +00:00
dirpath := strings . Join ( parts [ : ( len ( parts ) - 1 ) ] , "/" )
2020-04-10 19:20:53 +00:00
// TODO(q3k): figure out this hack, hopefully by implementing a real path type
if dirpath == "/" {
dirpath = "//"
}
2020-04-10 15:39:43 +00:00
cfg , err := config . ForPath ( r . ctx , r . source , dirpath )
2020-04-08 18:03:12 +00:00
if err != nil {
glog . Errorf ( "could not get config for path %q: %w" , dirpath , err )
2020-04-10 15:39:43 +00:00
r . handle500 ( )
2020-04-08 18:03:12 +00:00
return
}
2020-04-10 19:20:53 +00:00
r . handleFile ( r . rpath , cfg )
2020-04-08 18:03:12 +00:00
return
}
// Otherwise assume directory, try all posibilities.
2020-04-10 15:39:43 +00:00
dirpath := r . rpath
if r . rpath != "//" {
dirpath = strings . TrimSuffix ( r . rpath , "/" ) + "/"
2020-04-08 18:03:12 +00:00
}
2020-04-10 15:39:43 +00:00
r . handlePageAuto ( dirpath )
2020-04-08 18:03:12 +00:00
}