Skip to content

Commit f79e4fc

Browse files
authored
Add support for fully offline binary via compile-time 'offline' tag (ddworken#272)
* Add support for fully offline binary via compile-time 'offline' tag * Update docs
1 parent ffc224e commit f79e4fc

11 files changed

Lines changed: 118 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ curl https://hishtory.dev/install.py | python3 - --offline
165165

166166
This disables syncing completely so that the client will not rely on the hiSHtory backend at all. You can also change the syncing status via `hishtory syncing enable` or `hishtory syncing disable`.
167167

168+
For more information on offline mode, see [here](https://github.com/ddworken/hishtory/blob/master/docs/offline-binary.md).
169+
168170
</blockquote></details>
169171

170172
<details>

‎client/client_test.go‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ func TestMain(m *testing.M) {
5454
panic(fmt.Sprintf("failed to build client: %v", err))
5555
}
5656

57+
// Build the fully offline client so it is available in /tmp/client-offline
58+
cmd = exec.Command("go", "build", "-o", "/tmp/client-offline", "-tags", "offline")
59+
cmd.Env = os.Environ()
60+
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
61+
err = cmd.Run()
62+
if err != nil {
63+
panic(fmt.Sprintf("failed to build offline client: %v", err))
64+
}
65+
5766
// Start the tests
5867
m.Run()
5968
}
@@ -3434,7 +3443,7 @@ func TestStatusFullConfig(t *testing.T) {
34343443
out := tester.RunInteractiveShell(t, `hishtory status --full-config | grep -v 'Secret Key'`)
34353444
testutils.CompareGoldens(t, out, "TestStatusFullConfig")
34363445
}
3437-
3446+
34383447
func TestExportJson(t *testing.T) {
34393448
markTestForSharding(t, 20)
34403449
defer testutils.BackupAndRestore(t)()
@@ -3470,4 +3479,58 @@ func TestImportJson(t *testing.T) {
34703479
testutils.CompareGoldens(t, out, "TestExportJson")
34713480
}
34723481

3482+
func TestOfflineClient(t *testing.T) {
3483+
markTestForSharding(t, 21)
3484+
defer testutils.BackupAndRestore(t)()
3485+
tester := zshTester{}
3486+
3487+
// Install the offline client
3488+
out := tester.RunInteractiveShell(t, ` /tmp/client-offline install `)
3489+
r := regexp.MustCompile(`Setting secret hishtory key to (.*)`)
3490+
matches := r.FindStringSubmatch(out)
3491+
if len(matches) != 2 {
3492+
t.Fatalf("Failed to extract userSecret from output=%#v: matches=%#v", out, matches)
3493+
}
3494+
assertOnlineStatus(t, Offline)
3495+
3496+
// Disable recording so that all our testing commands don't get recorded
3497+
_, _ = tester.RunInteractiveShellRelaxed(t, ` hishtory disable`)
3498+
_, _ = tester.RunInteractiveShellRelaxed(t, `hishtory config-set enable-control-r true`)
3499+
tester.RunInteractiveShell(t, ` HISHTORY_REDACT_FORCE=true hishtory redact set emo pipefail`)
3500+
3501+
// Insert a few hishtory entries that we'll use for testing into an empty DB
3502+
db := hctx.GetDb(hctx.MakeContext())
3503+
require.NoError(t, db.Where("true").Delete(&data.HistoryEntry{}).Error)
3504+
e1 := testutils.MakeFakeHistoryEntry("ls ~/")
3505+
e1.CurrentWorkingDirectory = "/etc/"
3506+
e1.Hostname = "server"
3507+
e1.ExitCode = 127
3508+
require.NoError(t, db.Create(e1).Error)
3509+
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("ls ~/foo/")).Error)
3510+
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("ls ~/bar/")).Error)
3511+
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("echo 'aaaaaa bbbb'")).Error)
3512+
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("echo 'bar' &")).Error)
3513+
3514+
// Check that they're there (and there aren't any other entries)
3515+
var historyEntries []*data.HistoryEntry
3516+
db.Model(&data.HistoryEntry{}).Find(&historyEntries)
3517+
if len(historyEntries) != 5 {
3518+
t.Fatalf("expected to find 6 history entries, actual found %d: %#v", len(historyEntries), historyEntries)
3519+
}
3520+
out = tester.RunInteractiveShell(t, `hishtory export`)
3521+
testutils.CompareGoldens(t, out, "testControlR-InitialExport")
3522+
3523+
// And check that the control-r binding brings up the search
3524+
out = captureTerminalOutputWithShellName(t, tester, tester.ShellName(), []string{"C-R"})
3525+
split := strings.Split(out, "\n\n\n")
3526+
out = strings.TrimSpace(split[len(split)-1])
3527+
testutils.CompareGoldens(t, out, "testControlR-Initial")
3528+
3529+
// And check that even if syncing is enabled, the fully offline client will never send an HTTP request
3530+
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory syncing enable`)
3531+
require.Error(t, err)
3532+
require.Contains(t, err.Error(), "panic: Cannot GetHttpClient() from a hishtory client compiled with the offline tag!")
3533+
}
3534+
3535+
34733536
// TODO: somehow test/confirm that hishtory works even if only bash/only zsh is installed

‎client/cmd/install.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ var installCmd = &cobra.Command{
4949
if strings.HasPrefix(secretKey, "-") {
5050
lib.CheckFatalError(fmt.Errorf("secret key %#v looks like a CLI flag, please use a secret key that does not start with a -", secretKey))
5151
}
52-
lib.CheckFatalError(install(secretKey, *offlineInstall, *skipConfigModification || *skipUpdateConfigModification))
52+
lib.CheckFatalError(install(secretKey, *offlineInstall || lib.IsOfflineBinary(), *skipConfigModification || *skipUpdateConfigModification))
5353
if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" {
5454
db, err := hctx.OpenLocalSqliteDb()
5555
lib.CheckFatalError(err)

‎client/cmd/redact.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var redactCmd = &cobra.Command{
2828
Run: func(cmd *cobra.Command, args []string) {
2929
ctx := hctx.MakeContext()
3030
skipOnlineRedaction := false
31-
if !lib.CanReachHishtoryServer(ctx) {
31+
if !hctx.GetConf(ctx).IsOffline && !lib.CanReachHishtoryServer(ctx) {
3232
fmt.Printf("Cannot reach hishtory backend (is this device offline?) so redaction will only apply to this device and not other synced devices. Would you like to continue with a local-only redaction anyways? [y/N] ")
3333
reader := bufio.NewReader(os.Stdin)
3434
resp, err := reader.ReadString('\n')

‎client/cmd/syncing.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
var syncingCmd = &cobra.Command{
1515
Use: "syncing",
1616
Short: "Configure syncing to enable or disable syncing with the hishtory backend",
17+
Long: "Run `hishtory syncing disable` to disable syncing and `hishtory syncing enable` to enable syncing.",
1718
ValidArgs: []string{"disable", "enable"},
1819
Args: cobra.MatchAll(cobra.OnlyValidArgs, cobra.ExactArgs(1)),
1920
Run: func(cmd *cobra.Command, args []string) {

‎client/cmd/update.go‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9-
"net/http"
109
"os"
1110
"os/exec"
1211
"path"
@@ -287,7 +286,7 @@ func downloadFile(filename, url string) error {
287286
}
288287

289288
// Download the data
290-
resp, err := http.Get(url)
289+
resp, err := lib.GetHttpClient().Get(url)
291290
if err != nil {
292291
return fmt.Errorf("failed to download file at %s to %s: %w", url, filename, err)
293292
}

‎client/lib/lib.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ func ApiGet(ctx context.Context, path string) ([]byte, error) {
460460
req.Header.Set("X-Hishtory-Version", "v0."+Version)
461461
req.Header.Set("X-Hishtory-Device-Id", hctx.GetConf(ctx).DeviceId)
462462
req.Header.Set("X-Hishtory-User-Id", data.UserId(hctx.GetConf(ctx).UserSecret))
463-
resp, err := http.DefaultClient.Do(req)
463+
resp, err := GetHttpClient().Do(req)
464464
if err != nil {
465465
return nil, fmt.Errorf("failed to GET %s%s: %w", GetServerHostname(), path, err)
466466
}
@@ -490,7 +490,7 @@ func ApiPost(ctx context.Context, path, contentType string, reqBody []byte) ([]b
490490
req.Header.Set("X-Hishtory-Version", "v0."+Version)
491491
req.Header.Set("X-Hishtory-Device-Id", hctx.GetConf(ctx).DeviceId)
492492
req.Header.Set("X-Hishtory-User-Id", data.UserId(hctx.GetConf(ctx).UserSecret))
493-
resp, err := http.DefaultClient.Do(req)
493+
resp, err := GetHttpClient().Do(req)
494494
if err != nil {
495495
return nil, fmt.Errorf("failed to POST %s: %w", GetServerHostname()+path, err)
496496
}

‎client/lib/net.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//go:build !offline
2+
// +build !offline
3+
4+
package lib
5+
6+
import (
7+
"net/http"
8+
)
9+
10+
func GetHttpClient() *http.Client {
11+
return http.DefaultClient
12+
}
13+
14+
func IsOfflineBinary() bool {
15+
return false
16+
}

‎client/lib/net_disabled.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build offline
2+
// +build offline
3+
4+
package lib
5+
6+
import "net/http"
7+
8+
func GetHttpClient() *http.Client {
9+
panic("Cannot GetHttpClient() from a hishtory client compiled with the offline tag!")
10+
}
11+
12+
func IsOfflineBinary() bool {
13+
return true
14+
}

‎docs/offline-binary.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Offline Binary
2+
3+
hiSHtory supports disabling syncing at install-time via `curl https://hishtory.dev/install.py | python3 - --offline` or at config-time via `hishtory syncing disable`. This will disable persisting your (encrypted) history on the backend API server. For most users, this is the recommended option for running hiSHtory in an offline environment since it still supports opt-in updates via `hishtory update`.
4+
5+
But, if you need stronger guarantees that hiSHtory will not make any network requests, this can also be done by compiling your own copy of hiSHtory with the `offline` tag. This will statically link in `net_disabled.go` which will guarantee that the binary cannot make any HTTP requests. To use this:
6+
7+
```
8+
git clone https://github.com/ddworken/hishtory
9+
cd hishtory
10+
go build -tags offline
11+
./hishtory install
12+
```
13+
14+
This binary will be entirely offline and is guaranteed to never make any requests to `api.hishtory.dev`.

0 commit comments

Comments
 (0)