2022-01-15 16:32:49 +00:00
package main
import (
"context"
"crypto/tls"
"database/sql"
2022-01-17 13:55:31 +00:00
"embed"
2022-01-15 16:32:49 +00:00
"encoding/json"
2022-01-17 15:46:51 +00:00
"io"
2022-01-15 16:32:49 +00:00
"net/http"
"time"
"tailscale.com/client/tailscale"
2022-01-17 13:55:31 +00:00
"tailscale.com/client/tailscale/apitype"
2022-01-15 16:32:49 +00:00
"tailscale.com/tsnet"
"within.website/ln"
"within.website/ln/ex"
)
2022-01-17 13:55:31 +00:00
//go:embed static/*
var staticFS embed . FS
func withWhois ( next func ( whois * apitype . WhoIsResponse , w http . ResponseWriter , r * http . Request ) ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
userInfo , err := tailscale . WhoIs ( r . Context ( ) , r . RemoteAddr )
if err != nil {
http . Error ( w , "can't get whois response: " + err . Error ( ) , http . StatusInternalServerError )
ln . Error ( r . Context ( ) , err )
return
}
next ( userInfo , w , r )
} )
}
2022-01-15 16:32:49 +00:00
type Server struct {
db * sql . DB
mux * http . ServeMux
srv * tsnet . Server
}
func NewServer ( db * sql . DB , srv * tsnet . Server ) * Server {
mux := http . NewServeMux ( )
s := & Server {
db : db ,
mux : mux ,
srv : srv ,
}
2022-01-17 15:46:51 +00:00
mux . HandleFunc ( "/schema.sql" , func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "text/plain" )
fin , err := schemaFS . Open ( "schema/schema.sql" )
if err != nil {
http . Error ( w , "not found??" , http . StatusNotFound )
return
}
defer fin . Close ( )
io . Copy ( w , fin )
} )
2022-01-15 16:32:49 +00:00
mux . HandleFunc ( "/api/whois" , s . whois )
2022-01-17 13:55:31 +00:00
mux . HandleFunc ( "/api/importcsv" , s . importCSV )
mux . HandleFunc ( "/api/queries/get" , s . getQueries )
mux . Handle ( "/static/" , http . FileServer ( http . FS ( staticFS ) ) )
2022-01-15 16:32:49 +00:00
return s
}
func ( s * Server ) ListenAndServe ( ) error {
l , err := s . srv . Listen ( "tcp" , ":443" )
if err != nil {
return err
}
l = tls . NewListener ( l , & tls . Config {
GetCertificate : func ( chi * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
c , err := tailscale . GetCertificate ( chi )
if err != nil {
ln . Error ( context . Background ( ) , err , ln . F { "remote_addr" : chi . Conn . RemoteAddr ( ) . String ( ) } )
}
return c , err
} ,
} )
hs := & http . Server {
IdleTimeout : 5 * time . Minute ,
Handler : ex . HTTPLog ( s ) ,
}
ln . Log ( context . Background ( ) , ln . Info ( "listening on https://twitchalitics.shark-harmonic.ts.net" ) )
return hs . Serve ( l )
}
2022-01-17 13:55:31 +00:00
func ( s * Server ) whois ( w http . ResponseWriter , r * http . Request ) {
2022-01-15 16:32:49 +00:00
userInfo , err := tailscale . WhoIs ( r . Context ( ) , r . RemoteAddr )
if err != nil {
http . Error ( w , "can't get whois response: " + err . Error ( ) , http . StatusInternalServerError )
ln . Error ( r . Context ( ) , err )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2022-01-17 13:55:31 +00:00
w . WriteHeader ( http . StatusOK )
2022-01-15 16:32:49 +00:00
json . NewEncoder ( w ) . Encode ( userInfo )
}
2022-01-17 13:55:31 +00:00
func ( s * Server ) addQuery ( whois * apitype . WhoIsResponse , w http . ResponseWriter , r * http . Request ) {
type AddQueryRequest struct {
Name string ` json:"name" `
Query string ` json:"query" `
}
var aqr AddQueryRequest
err := json . NewDecoder ( r . Body ) . Decode ( & aqr )
if err != nil {
ln . Error ( r . Context ( ) , err )
http . Error ( w , "bad json" , http . StatusBadRequest )
return
}
who := whois . UserProfile . LoginName
_ , err = s . db . Exec ( "INSERT INTO sql_queries(name, query, who) VALUES (?, ?, ?)" , aqr . Name , aqr . Query , who )
if err != nil {
ln . Error ( r . Context ( ) , err )
http . Error ( w , "can't insert query" , http . StatusInternalServerError )
return
}
}
func ( s * Server ) getQueries ( w http . ResponseWriter , r * http . Request ) {
type SQLQuery struct {
Name string ` json:"name" `
Query string ` json:"query" `
Who string ` json:"who" `
}
var queries [ ] SQLQuery
rows , err := s . db . QueryContext ( r . Context ( ) , "SELECT name, query, who FROM sql_queries" )
if err != nil {
http . Error ( w , "can't get query rows" , http . StatusInternalServerError )
ln . Error ( r . Context ( ) , err )
return
}
defer rows . Close ( )
for rows . Next ( ) {
var sq SQLQuery
err := rows . Scan ( & sq . Name , & sq . Query , & sq . Who )
if err != nil {
http . Error ( w , "can't get query row" , http . StatusInternalServerError )
ln . Error ( r . Context ( ) , err )
}
queries = append ( queries , sq )
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . WriteHeader ( http . StatusOK )
json . NewEncoder ( w ) . Encode ( queries )
}
// curl https://twitchalitics.shark-harmonic.ts.net/api/importcsv -v -F csv=@"Channel Analytics and Revenue by day from Dec_19_2021 to Jan_17_2022.csv"
func ( s * Server ) importCSV ( w http . ResponseWriter , r * http . Request ) {
tx , err := s . db . BeginTx ( r . Context ( ) , nil )
if err != nil {
ln . Error ( r . Context ( ) , err )
http . Error ( w , "can't hit db" , http . StatusInternalServerError )
return
}
defer tx . Rollback ( )
r . ParseMultipartForm ( 10 << 20 )
file , handler , err := r . FormFile ( "csv" )
if err != nil {
ln . Error ( r . Context ( ) , err )
http . Error ( w , "can't read file" , http . StatusInternalServerError )
}
ln . WithF ( r . Context ( ) , ln . F { "filename" : handler . Filename , "size" : handler . Size } )
rows , err := ReadRows ( file )
for _ , row := range rows {
err := row . Upsert ( tx )
if err != nil {
ln . Error ( r . Context ( ) , err )
http . Error ( w , "can't upsert row" , http . StatusInternalServerError )
return
}
}
err = tx . Commit ( )
if err != nil {
ln . Error ( r . Context ( ) , err )
http . Error ( w , "can't commit data" , http . StatusInternalServerError )
}
http . Redirect ( w , r , "/" , http . StatusTemporaryRedirect )
}
2022-01-15 16:32:49 +00:00
func ( s Server ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
userInfo , err := tailscale . WhoIs ( r . Context ( ) , r . RemoteAddr )
if err != nil {
http . Error ( w , "can't get whois response: " + err . Error ( ) , http . StatusInternalServerError )
ln . Error ( r . Context ( ) , err )
return
}
ctx := ln . WithF ( r . Context ( ) , ln . F { "username" : userInfo . UserProfile . LoginName , "hostname" : userInfo . Node . ComputedName , "os" : userInfo . Node . Hostinfo . OS } )
r = r . WithContext ( ctx )
s . mux . ServeHTTP ( w , r )
}