2022-01-09 05:27:18 +01:00
package main
import (
"encoding/json"
"fmt"
2022-04-06 08:31:24 +02:00
"io/ioutil"
2022-04-07 03:18:46 +02:00
"log"
2022-01-09 05:27:18 +01:00
"net/http"
2022-04-08 05:59:40 +02:00
"os"
2022-04-09 21:50:01 +02:00
"strings"
2022-04-09 08:47:13 +02:00
"time"
2022-01-09 05:27:18 +01:00
2022-01-09 06:59:28 +01:00
"github.com/ddworken/hishtory/shared"
2022-03-30 06:56:28 +02:00
_ "github.com/lib/pq"
"gorm.io/driver/postgres"
2022-04-06 08:31:24 +02:00
"gorm.io/driver/sqlite"
2022-03-30 06:56:28 +02:00
"gorm.io/gorm"
)
const (
2022-04-09 09:33:20 +02:00
// This password is for the postgres cluster running in my k8s cluster that is not publicly accessible. Some day I'll migrate it out of the code and rotate the password, but until then, this is fine.
2022-04-09 07:37:03 +02:00
PostgresDb = "postgresql://postgres:O74Ji4735C@postgres-postgresql.default.svc.cluster.local:5432/hishtory?sslmode=disable"
2022-01-09 06:59:28 +01:00
)
2022-01-09 05:27:18 +01:00
2022-04-09 07:37:03 +02:00
var (
GLOBAL_DB * gorm . DB
ReleaseVersion string = "UNKNOWN"
)
2022-04-03 07:27:20 +02:00
2022-04-16 20:37:43 +02:00
type UsageData struct {
UserId string ` json:"user_id" gorm:"not null; uniqueIndex:usageDataUniqueIndex" `
DeviceId string ` json:"device_id" gorm:"not null; uniqueIndex:usageDataUniqueIndex" `
LastUsed time . Time ` json:"last_used" `
}
func updateUsageData ( userId , deviceId string ) {
var usageData [ ] UsageData
GLOBAL_DB . Where ( "user_id = ? AND device_id = ?" , userId , deviceId ) . Find ( & usageData )
if len ( usageData ) == 0 {
GLOBAL_DB . Create ( & UsageData { UserId : userId , DeviceId : deviceId , LastUsed : time . Now ( ) } )
} else {
GLOBAL_DB . Model ( & UsageData { } ) . Where ( "user_id = ? AND device_id = ?" , userId , deviceId ) . Update ( "last_used" , time . Now ( ) )
}
}
2022-04-03 07:27:20 +02:00
func apiESubmitHandler ( w http . ResponseWriter , r * http . Request ) {
2022-04-06 08:31:24 +02:00
data , err := ioutil . ReadAll ( r . Body )
2022-03-30 06:56:28 +02:00
if err != nil {
panic ( err )
}
2022-04-06 08:31:24 +02:00
var entries [ ] shared . EncHistoryEntry
err = json . Unmarshal ( data , & entries )
if err != nil {
panic ( fmt . Sprintf ( "body=%#v, err=%v" , data , err ) )
}
2022-04-07 07:43:07 +02:00
fmt . Printf ( "apiESubmitHandler: received request containg %d EncHistoryEntry\n" , len ( entries ) )
2022-04-03 07:27:20 +02:00
for _ , entry := range entries {
2022-04-16 20:37:43 +02:00
updateUsageData ( entry . UserId , entry . DeviceId )
2022-04-03 19:08:18 +02:00
tx := GLOBAL_DB . Where ( "user_id = ?" , entry . UserId )
2022-04-04 05:55:37 +02:00
var devices [ ] * shared . Device
2022-04-03 19:08:18 +02:00
result := tx . Find ( & devices )
if result . Error != nil {
panic ( fmt . Errorf ( "DB query error: %v" , result . Error ) )
}
2022-04-04 05:55:37 +02:00
if len ( devices ) == 0 {
2022-04-08 06:40:22 +02:00
panic ( fmt . Errorf ( "found no devices associated with user_id=%s, can't save history entry" , entry . UserId ) )
2022-04-03 19:08:18 +02:00
}
2022-04-07 07:43:07 +02:00
fmt . Printf ( "apiESubmitHandler: Found %d devices\n" , len ( devices ) )
2022-04-03 19:08:18 +02:00
for _ , device := range devices {
2022-04-04 05:55:37 +02:00
entry . DeviceId = device . DeviceId
2022-04-07 07:43:07 +02:00
result := GLOBAL_DB . Create ( & entry )
if result . Error != nil {
panic ( result . Error )
}
2022-04-03 19:08:18 +02:00
}
2022-04-03 07:27:20 +02:00
}
}
func apiEQueryHandler ( w http . ResponseWriter , r * http . Request ) {
2022-04-16 20:37:43 +02:00
userId := r . URL . Query ( ) . Get ( "user_id" )
2022-04-03 07:27:20 +02:00
deviceId := r . URL . Query ( ) . Get ( "device_id" )
2022-04-16 20:37:43 +02:00
updateUsageData ( userId , deviceId )
2022-04-04 05:55:37 +02:00
// Increment the count
2022-04-03 07:27:20 +02:00
GLOBAL_DB . Exec ( "UPDATE enc_history_entries SET read_count = read_count + 1 WHERE device_id = ?" , deviceId )
// Then retrieve, to avoid a race condition
2022-04-16 19:30:39 +02:00
tx := GLOBAL_DB . Where ( "device_id = ? AND read_count < 5" , deviceId )
2022-04-03 07:27:20 +02:00
var historyEntries [ ] * shared . EncHistoryEntry
result := tx . Find ( & historyEntries )
if result . Error != nil {
panic ( fmt . Errorf ( "DB query error: %v" , result . Error ) )
}
2022-04-07 07:43:07 +02:00
fmt . Printf ( "apiEQueryHandler: Found %d entries\n" , len ( historyEntries ) )
2022-04-03 07:27:20 +02:00
resp , err := json . Marshal ( historyEntries )
if err != nil {
panic ( err )
}
w . Write ( resp )
}
2022-04-06 08:31:24 +02:00
// TODO: bootstrap is a janky solution for the initial version of this. Long term, need to support deleting entries from the DB which means replacing bootstrap with a queued message sent to any live instances.
2022-04-03 07:27:20 +02:00
func apiEBootstrapHandler ( w http . ResponseWriter , r * http . Request ) {
userId := r . URL . Query ( ) . Get ( "user_id" )
2022-04-16 20:37:43 +02:00
deviceId := r . URL . Query ( ) . Get ( "device_id" )
updateUsageData ( userId , deviceId )
2022-04-03 07:27:20 +02:00
tx := GLOBAL_DB . Where ( "user_id = ?" , userId )
var historyEntries [ ] * shared . EncHistoryEntry
result := tx . Find ( & historyEntries )
if result . Error != nil {
panic ( fmt . Errorf ( "DB query error: %v" , result . Error ) )
}
resp , err := json . Marshal ( historyEntries )
2022-01-09 05:27:18 +01:00
if err != nil {
panic ( err )
}
2022-04-03 07:27:20 +02:00
w . Write ( resp )
}
2022-04-03 19:08:18 +02:00
func apiERegisterHandler ( w http . ResponseWriter , r * http . Request ) {
userId := r . URL . Query ( ) . Get ( "user_id" )
deviceId := r . URL . Query ( ) . Get ( "device_id" )
2022-04-10 01:37:51 +02:00
GLOBAL_DB . Create ( & shared . Device { UserId : userId , DeviceId : deviceId , RegistrationIp : r . RemoteAddr , RegistrationDate : time . Now ( ) } )
2022-04-16 20:37:43 +02:00
updateUsageData ( userId , deviceId )
2022-01-09 05:27:18 +01:00
}
2022-04-07 07:43:07 +02:00
func apiBannerHandler ( w http . ResponseWriter , r * http . Request ) {
commitHash := r . URL . Query ( ) . Get ( "commit_hash" )
deviceId := r . URL . Query ( ) . Get ( "device_id" )
forcedBanner := r . URL . Query ( ) . Get ( "forced_banner" )
fmt . Printf ( "apiBannerHandler: commit_hash=%#v, device_id=%#v, forced_banner=%#v\n" , commitHash , deviceId , forcedBanner )
w . Write ( [ ] byte ( forcedBanner ) )
}
2022-04-08 05:59:40 +02:00
func isTestEnvironment ( ) bool {
return os . Getenv ( "HISHTORY_TEST" ) != ""
}
2022-03-30 06:56:28 +02:00
func OpenDB ( ) ( * gorm . DB , error ) {
2022-04-08 05:59:40 +02:00
if isTestEnvironment ( ) {
2022-04-06 08:31:24 +02:00
db , err := gorm . Open ( sqlite . Open ( "file::memory:?cache=shared" ) , & gorm . Config { } )
if err != nil {
return nil , fmt . Errorf ( "failed to connect to the DB: %v" , err )
}
db . AutoMigrate ( & shared . EncHistoryEntry { } )
db . AutoMigrate ( & shared . Device { } )
2022-04-16 20:37:43 +02:00
db . AutoMigrate ( & UsageData { } )
2022-04-06 08:31:24 +02:00
return db , nil
2022-03-30 06:56:28 +02:00
}
2022-04-09 07:37:03 +02:00
db , err := gorm . Open ( postgres . Open ( PostgresDb ) , & gorm . Config { } )
2022-03-30 06:56:28 +02:00
if err != nil {
return nil , fmt . Errorf ( "failed to connect to the DB: %v" , err )
}
2022-04-03 07:27:20 +02:00
db . AutoMigrate ( & shared . EncHistoryEntry { } )
db . AutoMigrate ( & shared . Device { } )
2022-04-16 20:37:43 +02:00
db . AutoMigrate ( & UsageData { } )
2022-03-30 06:56:28 +02:00
return db , nil
}
2022-04-03 07:27:20 +02:00
func init ( ) {
2022-04-09 08:47:13 +02:00
if ReleaseVersion == "UNKNOWN" && ! isTestEnvironment ( ) {
2022-04-09 07:37:03 +02:00
panic ( "server.go was built without a ReleaseVersion!" )
}
2022-04-03 07:54:09 +02:00
InitDB ( )
2022-04-16 20:37:43 +02:00
go runBackgroundJobs ( )
2022-04-03 07:54:09 +02:00
}
2022-04-16 20:37:43 +02:00
func runBackgroundJobs ( ) {
2022-04-09 08:47:13 +02:00
for {
2022-04-09 21:57:58 +02:00
err := updateReleaseVersion ( )
if err != nil {
fmt . Println ( err )
}
2022-04-16 20:37:43 +02:00
err = cleanDatabase ( )
if err != nil {
fmt . Println ( err )
}
2022-04-09 08:47:13 +02:00
time . Sleep ( 10 * time . Minute )
}
}
type releaseInfo struct {
Name string ` json:"name" `
}
2022-04-09 21:57:58 +02:00
func updateReleaseVersion ( ) error {
2022-04-09 08:47:13 +02:00
resp , err := http . Get ( "https://api.github.com/repos/ddworken/hishtory/releases/latest" )
if err != nil {
2022-04-09 22:02:30 +02:00
return fmt . Errorf ( "failed to get latest release version: %v" , err )
2022-04-09 08:47:13 +02:00
}
respBody , err := ioutil . ReadAll ( resp . Body )
if err != nil {
2022-04-09 22:02:30 +02:00
return fmt . Errorf ( "failed to read github API response body: %v" , err )
2022-04-09 08:47:13 +02:00
}
2022-04-09 21:50:01 +02:00
if resp . StatusCode == 403 && strings . Contains ( string ( respBody ) , "API rate limit exceeded for " ) {
2022-04-09 21:57:58 +02:00
return nil
2022-04-09 21:50:01 +02:00
}
if resp . StatusCode != 200 {
2022-04-09 22:02:30 +02:00
return fmt . Errorf ( "failed to call github API, status_code=%d, body=%#v" , resp . StatusCode , string ( respBody ) )
2022-04-09 21:50:01 +02:00
}
2022-04-09 08:47:13 +02:00
var info releaseInfo
err = json . Unmarshal ( respBody , & info )
if err != nil {
2022-04-09 21:57:58 +02:00
return fmt . Errorf ( "failed to parse github API response: %v" , err )
2022-04-09 08:47:13 +02:00
}
ReleaseVersion = info . Name
2022-04-09 21:57:58 +02:00
return nil
2022-04-09 08:47:13 +02:00
}
2022-04-03 07:54:09 +02:00
func InitDB ( ) {
2022-04-03 07:27:20 +02:00
var err error
GLOBAL_DB , err = OpenDB ( )
2022-01-09 05:27:18 +01:00
if err != nil {
panic ( err )
}
2022-04-03 07:27:20 +02:00
tx , err := GLOBAL_DB . DB ( )
if err != nil {
panic ( err )
}
err = tx . Ping ( )
if err != nil {
panic ( err )
}
}
2022-04-09 07:37:03 +02:00
func bindaryDownloadHandler ( w http . ResponseWriter , r * http . Request ) {
http . Redirect ( w , r , fmt . Sprintf ( "https://github.com/ddworken/hishtory/releases/download/%s/hishtory-linux-amd64" , ReleaseVersion ) , http . StatusFound )
}
func attestationDownloadHandler ( w http . ResponseWriter , r * http . Request ) {
http . Redirect ( w , r , fmt . Sprintf ( "https://github.com/ddworken/hishtory/releases/download/%s/hishtory-linux-amd64.intoto.jsonl" , ReleaseVersion ) , http . StatusFound )
}
2022-04-16 09:44:47 +02:00
type loggedResponseData struct {
size int
}
type loggingResponseWriter struct {
http . ResponseWriter
responseData * loggedResponseData
}
func ( r * loggingResponseWriter ) Write ( b [ ] byte ) ( int , error ) {
size , err := r . ResponseWriter . Write ( b )
r . responseData . size += size
return size , err
}
func ( r * loggingResponseWriter ) WriteHeader ( statusCode int ) {
r . ResponseWriter . WriteHeader ( statusCode )
}
func withLogging ( h func ( http . ResponseWriter , * http . Request ) ) http . Handler {
2022-04-16 08:19:39 +02:00
logFn := func ( rw http . ResponseWriter , r * http . Request ) {
2022-04-16 09:44:47 +02:00
var responseData loggedResponseData
lrw := loggingResponseWriter {
ResponseWriter : rw ,
responseData : & responseData ,
}
2022-04-16 08:19:39 +02:00
start := time . Now ( )
2022-04-16 09:44:47 +02:00
h ( & lrw , r )
2022-04-16 08:19:39 +02:00
duration := time . Since ( start )
2022-04-16 09:44:47 +02:00
fmt . Printf ( "%s %s %#v %s %s\n" , r . RemoteAddr , r . Method , r . RequestURI , duration . String ( ) , byteCountToString ( responseData . size ) )
2022-04-16 08:19:39 +02:00
}
return http . HandlerFunc ( logFn )
}
2022-04-16 09:44:47 +02:00
func byteCountToString ( b int ) string {
const unit = 1000
if b < unit {
return fmt . Sprintf ( "%d B" , b )
}
div , exp := int64 ( unit ) , 0
for n := b / unit ; n >= unit ; n /= unit {
div *= unit
exp ++
}
return fmt . Sprintf ( "%.1f %cB" , float64 ( b ) / float64 ( div ) , "kMG" [ exp ] )
}
2022-04-16 20:37:43 +02:00
func cleanDatabase ( ) error {
result := GLOBAL_DB . Exec ( "DELETE FROM enc_history_entries WHERE read_count > 10" )
if result . Error != nil {
return result . Error
}
return nil
}
2022-04-03 07:27:20 +02:00
func main ( ) {
2022-01-09 05:27:18 +01:00
fmt . Println ( "Listening on localhost:8080" )
2022-04-16 08:19:39 +02:00
http . Handle ( "/api/v1/esubmit" , withLogging ( apiESubmitHandler ) )
http . Handle ( "/api/v1/equery" , withLogging ( apiEQueryHandler ) )
http . Handle ( "/api/v1/ebootstrap" , withLogging ( apiEBootstrapHandler ) )
http . Handle ( "/api/v1/eregister" , withLogging ( apiERegisterHandler ) )
http . Handle ( "/api/v1/banner" , withLogging ( apiBannerHandler ) )
http . Handle ( "/download/hishtory-linux-amd64" , withLogging ( bindaryDownloadHandler ) )
http . Handle ( "/download/hishtory-linux-amd64.intoto.jsonl" , withLogging ( attestationDownloadHandler ) )
2022-01-09 05:27:18 +01:00
log . Fatal ( http . ListenAndServe ( ":8080" , nil ) )
}