keystroke-sender 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.x"
19
+
20
+ - name: Install build dependencies
21
+ run: pip install build
22
+
23
+ - name: Build package
24
+ run: python -m build
25
+
26
+ - name: Upload build artifacts
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish:
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ permissions:
37
+ id-token: write
38
+ steps:
39
+ - name: Download build artifacts
40
+ uses: actions/download-artifact@v4
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+
45
+ - name: Publish to PyPI
46
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .env
5
+ venv/
6
+ .venv/
7
+ keystroke_sender_wrapper.bat
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ nul
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: keystroke-sender
3
+ Version: 0.1.0
4
+ Summary: Chrome Native Messaging host that simulates OS-level keystrokes and mouse clicks
5
+ Project-URL: Repository, https://github.com/PropDream/keystroke-sender
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.7
8
+ Requires-Dist: pynput
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Keystroke Sender
12
+
13
+ A Chrome Native Messaging host that simulates OS-level keystrokes. Receives text strings from a Chrome extension and types them out as real key presses using `pynput`.
14
+
15
+ Works on **macOS**, **Linux**, and **Windows**.
16
+
17
+ Companion extension: [Chrome Form Filler](https://chromewebstore.google.com/detail/chrome-form-filler/dpdolkkncejkelemckjmjoaefmgdhepj) (`dpdolkkncejkelemckjmjoaefmgdhepj`)
18
+
19
+ ## Prerequisites
20
+
21
+ - Python 3.7+
22
+ - pip
23
+ - Google Chrome
24
+ - [Chrome Form Filler](https://chromewebstore.google.com/detail/chrome-form-filler/dpdolkkncejkelemckjmjoaefmgdhepj) extension
25
+
26
+ ## Installation
27
+
28
+ ### pip install (recommended)
29
+
30
+ ```bash
31
+ pip install git+https://github.com/PropDream/keystroke-sender.git
32
+ ```
33
+
34
+ Then register the Chrome native messaging host:
35
+
36
+ ```bash
37
+ keystroke-sender-register YOUR_EXTENSION_ID
38
+ ```
39
+
40
+ To unregister later:
41
+
42
+ ```bash
43
+ keystroke-sender-register --unregister
44
+ ```
45
+
46
+ ### Manual install (macOS / Linux)
47
+
48
+ ```bash
49
+ chmod +x install.sh
50
+ ./install.sh
51
+ ```
52
+
53
+ ### Manual install (Windows)
54
+
55
+ ```cmd
56
+ install.bat
57
+ ```
58
+
59
+ The manual installer will:
60
+ 1. Prompt you for your Chrome extension ID (find it at `chrome://extensions`)
61
+ 2. Create the native messaging host manifest in the correct OS location
62
+ 3. Install the `pynput` Python dependency
63
+
64
+ ### macOS Accessibility Permission
65
+
66
+ On macOS, you must grant Accessibility permission to your terminal app (or Python) for keystroke simulation to work:
67
+
68
+ **System Preferences > Privacy & Security > Accessibility** — add your terminal app (Terminal, iTerm2, etc.).
69
+
70
+ ## Usage
71
+
72
+ From a Chrome extension, send a message to the native host:
73
+
74
+ ```javascript
75
+ chrome.runtime.sendNativeMessage(
76
+ "com.propdream.keystroke_sender",
77
+ { text: "Hello, world!" },
78
+ (response) => {
79
+ console.log(response);
80
+ // { status: "ok", typed: 13 }
81
+ }
82
+ );
83
+ ```
84
+
85
+ ### Message Format
86
+
87
+ **Request:**
88
+ ```json
89
+ {
90
+ "text": "string to type",
91
+ "delay": 0.05
92
+ }
93
+ ```
94
+
95
+ - `text` (required): The string to type as OS-level keystrokes.
96
+ - `delay` (optional): Seconds between each keystroke. Default: `0.05`.
97
+
98
+ **Response:**
99
+ ```json
100
+ { "status": "ok", "typed": 13 }
101
+ ```
102
+
103
+ or on error:
104
+ ```json
105
+ { "status": "error", "message": "description" }
106
+ ```
107
+
108
+ ## Extension Setup
109
+
110
+ Add `"nativeMessaging"` to your extension's `manifest.json` permissions:
111
+
112
+ ```json
113
+ {
114
+ "permissions": ["nativeMessaging"]
115
+ }
116
+ ```
117
+
118
+ ## Uninstallation
119
+
120
+ ### macOS / Linux
121
+
122
+ ```bash
123
+ chmod +x uninstall.sh
124
+ ./uninstall.sh
125
+ ```
126
+
127
+ ### Windows
128
+
129
+ ```cmd
130
+ uninstall.bat
131
+ ```
132
+
133
+ ## Security
134
+
135
+ This app simulates real keystrokes and mouse clicks at the OS level, so it includes several safeguards to prevent misuse:
136
+
137
+ **Chrome Native Messaging isolation** — The host process can *only* be launched by Chrome, and *only* the extension ID you registered in `allowed_origins` can send it messages. No network socket is opened; communication is purely via stdin/stdout. An unauthorized extension or external program cannot talk to it.
138
+
139
+ **Idle timeout** — The host automatically exits after **1 minute** of inactivity. It does not stay running indefinitely.
140
+
141
+ **Rate limiting** — A maximum of **100 actions per second** is enforced. Bursts beyond this are rejected with an error response.
142
+
143
+ **Input size limits** — Messages larger than **1 MB** are rejected before parsing. Text payloads longer than **10,000 characters** are rejected before typing.
144
+
145
+ | Limit | Default |
146
+ |---|---|
147
+ | Idle timeout | 60 s (1 min) |
148
+ | Rate limit | 100 actions / 1 s |
149
+ | Max message size | 1 MB |
150
+ | Max text length | 10,000 chars |
151
+
152
+ These constants are defined at the top of `src/keystroke_sender/host.py` and can be adjusted if needed.
153
+
154
+ ## Debugging
155
+
156
+ - Launch Chrome from the terminal to see native host stderr output
157
+ - Use `chrome://extensions` > Service Worker > Console to see extension-side errors
158
+ - Check `chrome.runtime.lastError` in the `sendNativeMessage` callback
@@ -0,0 +1,148 @@
1
+ # Keystroke Sender
2
+
3
+ A Chrome Native Messaging host that simulates OS-level keystrokes. Receives text strings from a Chrome extension and types them out as real key presses using `pynput`.
4
+
5
+ Works on **macOS**, **Linux**, and **Windows**.
6
+
7
+ Companion extension: [Chrome Form Filler](https://chromewebstore.google.com/detail/chrome-form-filler/dpdolkkncejkelemckjmjoaefmgdhepj) (`dpdolkkncejkelemckjmjoaefmgdhepj`)
8
+
9
+ ## Prerequisites
10
+
11
+ - Python 3.7+
12
+ - pip
13
+ - Google Chrome
14
+ - [Chrome Form Filler](https://chromewebstore.google.com/detail/chrome-form-filler/dpdolkkncejkelemckjmjoaefmgdhepj) extension
15
+
16
+ ## Installation
17
+
18
+ ### pip install (recommended)
19
+
20
+ ```bash
21
+ pip install git+https://github.com/PropDream/keystroke-sender.git
22
+ ```
23
+
24
+ Then register the Chrome native messaging host:
25
+
26
+ ```bash
27
+ keystroke-sender-register YOUR_EXTENSION_ID
28
+ ```
29
+
30
+ To unregister later:
31
+
32
+ ```bash
33
+ keystroke-sender-register --unregister
34
+ ```
35
+
36
+ ### Manual install (macOS / Linux)
37
+
38
+ ```bash
39
+ chmod +x install.sh
40
+ ./install.sh
41
+ ```
42
+
43
+ ### Manual install (Windows)
44
+
45
+ ```cmd
46
+ install.bat
47
+ ```
48
+
49
+ The manual installer will:
50
+ 1. Prompt you for your Chrome extension ID (find it at `chrome://extensions`)
51
+ 2. Create the native messaging host manifest in the correct OS location
52
+ 3. Install the `pynput` Python dependency
53
+
54
+ ### macOS Accessibility Permission
55
+
56
+ On macOS, you must grant Accessibility permission to your terminal app (or Python) for keystroke simulation to work:
57
+
58
+ **System Preferences > Privacy & Security > Accessibility** — add your terminal app (Terminal, iTerm2, etc.).
59
+
60
+ ## Usage
61
+
62
+ From a Chrome extension, send a message to the native host:
63
+
64
+ ```javascript
65
+ chrome.runtime.sendNativeMessage(
66
+ "com.propdream.keystroke_sender",
67
+ { text: "Hello, world!" },
68
+ (response) => {
69
+ console.log(response);
70
+ // { status: "ok", typed: 13 }
71
+ }
72
+ );
73
+ ```
74
+
75
+ ### Message Format
76
+
77
+ **Request:**
78
+ ```json
79
+ {
80
+ "text": "string to type",
81
+ "delay": 0.05
82
+ }
83
+ ```
84
+
85
+ - `text` (required): The string to type as OS-level keystrokes.
86
+ - `delay` (optional): Seconds between each keystroke. Default: `0.05`.
87
+
88
+ **Response:**
89
+ ```json
90
+ { "status": "ok", "typed": 13 }
91
+ ```
92
+
93
+ or on error:
94
+ ```json
95
+ { "status": "error", "message": "description" }
96
+ ```
97
+
98
+ ## Extension Setup
99
+
100
+ Add `"nativeMessaging"` to your extension's `manifest.json` permissions:
101
+
102
+ ```json
103
+ {
104
+ "permissions": ["nativeMessaging"]
105
+ }
106
+ ```
107
+
108
+ ## Uninstallation
109
+
110
+ ### macOS / Linux
111
+
112
+ ```bash
113
+ chmod +x uninstall.sh
114
+ ./uninstall.sh
115
+ ```
116
+
117
+ ### Windows
118
+
119
+ ```cmd
120
+ uninstall.bat
121
+ ```
122
+
123
+ ## Security
124
+
125
+ This app simulates real keystrokes and mouse clicks at the OS level, so it includes several safeguards to prevent misuse:
126
+
127
+ **Chrome Native Messaging isolation** — The host process can *only* be launched by Chrome, and *only* the extension ID you registered in `allowed_origins` can send it messages. No network socket is opened; communication is purely via stdin/stdout. An unauthorized extension or external program cannot talk to it.
128
+
129
+ **Idle timeout** — The host automatically exits after **1 minute** of inactivity. It does not stay running indefinitely.
130
+
131
+ **Rate limiting** — A maximum of **100 actions per second** is enforced. Bursts beyond this are rejected with an error response.
132
+
133
+ **Input size limits** — Messages larger than **1 MB** are rejected before parsing. Text payloads longer than **10,000 characters** are rejected before typing.
134
+
135
+ | Limit | Default |
136
+ |---|---|
137
+ | Idle timeout | 60 s (1 min) |
138
+ | Rate limit | 100 actions / 1 s |
139
+ | Max message size | 1 MB |
140
+ | Max text length | 10,000 chars |
141
+
142
+ These constants are defined at the top of `src/keystroke_sender/host.py` and can be adjusted if needed.
143
+
144
+ ## Debugging
145
+
146
+ - Launch Chrome from the terminal to see native host stderr output
147
+ - Use `chrome://extensions` > Service Worker > Console to see extension-side errors
148
+ - Check `chrome.runtime.lastError` in the `sendNativeMessage` callback
@@ -0,0 +1,53 @@
1
+ @echo off
2
+ REM install.bat — Install the Chrome Native Messaging host on Windows
3
+ setlocal enabledelayedexpansion
4
+
5
+ set HOST_NAME=com.propdream.keystroke_sender
6
+ set SCRIPT_DIR=%~dp0
7
+ set HOST_PATH=%SCRIPT_DIR%keystroke_sender_wrapper.bat
8
+
9
+ REM Create the wrapper batch file that Chrome will execute
10
+ echo @echo off > "%SCRIPT_DIR%keystroke_sender_wrapper.bat"
11
+ echo python "%SCRIPT_DIR%keystroke_sender.py" %%* >> "%SCRIPT_DIR%keystroke_sender_wrapper.bat"
12
+
13
+ REM Set manifest directory
14
+ set TARGET_DIR=%LOCALAPPDATA%\Google\Chrome\User Data\NativeMessagingHosts
15
+
16
+ REM Prompt for Chrome extension ID
17
+ set /p EXTENSION_ID="Enter your Chrome extension ID (found at chrome://extensions): "
18
+
19
+ if "%EXTENSION_ID%"=="" (
20
+ echo Error: Extension ID cannot be empty.
21
+ exit /b 1
22
+ )
23
+
24
+ REM Create target directory
25
+ if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%"
26
+
27
+ REM Write the native messaging host manifest
28
+ (
29
+ echo {
30
+ echo "name": "%HOST_NAME%",
31
+ echo "description": "Simulates OS-level keystrokes for Chrome Form Filler",
32
+ echo "path": "%HOST_PATH:\=\\%",
33
+ echo "type": "stdio",
34
+ echo "allowed_origins": [
35
+ echo "chrome-extension://%EXTENSION_ID%/"
36
+ echo ]
37
+ echo }
38
+ ) > "%TARGET_DIR%\%HOST_NAME%.json"
39
+
40
+ REM Add registry key (required on Windows)
41
+ reg add "HKCU\Software\Google\Chrome\NativeMessagingHosts\%HOST_NAME%" /ve /t REG_SZ /d "%TARGET_DIR%\%HOST_NAME%.json" /f
42
+
43
+ REM Install Python dependencies
44
+ echo Installing Python dependencies...
45
+ pip install -r "%SCRIPT_DIR%requirements.txt"
46
+
47
+ echo.
48
+ echo Native messaging host '%HOST_NAME%' installed successfully.
49
+ echo Manifest: %TARGET_DIR%\%HOST_NAME%.json
50
+ echo Host: %HOST_PATH%
51
+ echo Registry: HKCU\Software\Google\Chrome\NativeMessagingHosts\%HOST_NAME%
52
+
53
+ endlocal
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # install.sh — Install the Chrome Native Messaging host on macOS or Linux
3
+ set -e
4
+
5
+ HOST_NAME="com.propdream.keystroke_sender"
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ HOST_PATH="$SCRIPT_DIR/keystroke_sender.py"
8
+
9
+ # Detect OS and set manifest directory
10
+ if [[ "$OSTYPE" == "darwin"* ]]; then
11
+ TARGET_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
12
+ elif [[ "$OSTYPE" == "linux"* ]]; then
13
+ TARGET_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
14
+ else
15
+ echo "Unsupported OS: $OSTYPE"
16
+ echo "Use install.bat for Windows."
17
+ exit 1
18
+ fi
19
+
20
+ # Prompt for Chrome extension ID
21
+ echo "Enter your Chrome extension ID (found at chrome://extensions):"
22
+ read -r EXTENSION_ID
23
+
24
+ if [ -z "$EXTENSION_ID" ]; then
25
+ echo "Error: Extension ID cannot be empty."
26
+ exit 1
27
+ fi
28
+
29
+ # Create target directory
30
+ mkdir -p "$TARGET_DIR"
31
+
32
+ # Write the native messaging host manifest
33
+ cat > "$TARGET_DIR/$HOST_NAME.json" << EOF
34
+ {
35
+ "name": "$HOST_NAME",
36
+ "description": "Simulates OS-level keystrokes for Chrome Form Filler",
37
+ "path": "$HOST_PATH",
38
+ "type": "stdio",
39
+ "allowed_origins": [
40
+ "chrome-extension://$EXTENSION_ID/"
41
+ ]
42
+ }
43
+ EOF
44
+
45
+ # Make the host script executable
46
+ chmod +x "$HOST_PATH"
47
+
48
+ # Install Python dependencies
49
+ echo "Installing Python dependencies..."
50
+ pip3 install -r "$SCRIPT_DIR/requirements.txt"
51
+
52
+ echo ""
53
+ echo "Native messaging host '$HOST_NAME' installed successfully."
54
+ echo " Manifest: $TARGET_DIR/$HOST_NAME.json"
55
+ echo " Host: $HOST_PATH"
56
+ echo ""
57
+ if [[ "$OSTYPE" == "darwin"* ]]; then
58
+ echo "IMPORTANT: On macOS, you must grant Accessibility permission to your"
59
+ echo "terminal/Python in System Preferences > Privacy & Security > Accessibility."
60
+ fi
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "keystroke-sender"
7
+ version = "0.1.0"
8
+ description = "Chrome Native Messaging host that simulates OS-level keystrokes and mouse clicks"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = "MIT"
12
+ dependencies = ["pynput"]
13
+
14
+ [project.scripts]
15
+ keystroke-sender = "keystroke_sender.host:main"
16
+ keystroke-sender-register = "keystroke_sender.register:main"
17
+
18
+ [project.urls]
19
+ Repository = "https://github.com/PropDream/keystroke-sender"
@@ -0,0 +1 @@
1
+ pynput
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Chrome Native Messaging host that simulates OS-level keystrokes and mouse clicks.
4
+
5
+ Receives JSON messages from a Chrome extension via stdin and performs
6
+ OS-level input simulation using pynput. Works on macOS, Linux, and Windows.
7
+
8
+ Message protocol: 4-byte uint32 length prefix (native byte order) + UTF-8 JSON.
9
+
10
+ Actions:
11
+ type: {"action": "type", "text": "string to type", "delay": 0.05}
12
+ -> {"status": "ok", "typed": <count>}
13
+ click: {"action": "click", "x": 500, "y": 300}
14
+ -> {"status": "ok", "action": "click", "x": 500, "y": 300}
15
+
16
+ Legacy (no action field): {"text": "..."} is treated as type action.
17
+ Error response: {"status": "error", "message": "..."}
18
+ """
19
+
20
+ import sys
21
+ import os
22
+ import json
23
+ import struct
24
+ import threading
25
+ import time
26
+
27
+ # Ensure unbuffered stdout (critical for native messaging protocol)
28
+ if hasattr(sys.stdout, "reconfigure"):
29
+ sys.stdout.reconfigure(write_through=True)
30
+ os.environ["PYTHONUNBUFFERED"] = "1"
31
+
32
+ # --- Safety limits ---
33
+ MAX_MESSAGE_BYTES = 1_048_576 # 1 MB — reject anything larger
34
+ MAX_TEXT_LENGTH = 10_000 # max characters per type action
35
+ IDLE_TIMEOUT_SECONDS = 60 # exit after 1 min with no messages
36
+ RATE_LIMIT_WINDOW = 1.0 # rolling window in seconds
37
+ RATE_LIMIT_MAX = 100 # max actions per window
38
+
39
+
40
+ def log(text):
41
+ """Write debug output to stderr (never stdout)."""
42
+ sys.stderr.write(f"[keystroke-sender] {text}\n")
43
+ sys.stderr.flush()
44
+
45
+
46
+ def read_message():
47
+ """Read a single native messaging message from stdin.
48
+
49
+ Raises ValueError if the message exceeds MAX_MESSAGE_BYTES.
50
+ """
51
+ raw_length = sys.stdin.buffer.read(4)
52
+ if len(raw_length) == 0:
53
+ # Chrome disconnected
54
+ sys.exit(0)
55
+ if len(raw_length) < 4:
56
+ log(f"Expected 4 bytes for length, got {len(raw_length)}")
57
+ sys.exit(1)
58
+ message_length = struct.unpack("@I", raw_length)[0]
59
+ if message_length > MAX_MESSAGE_BYTES:
60
+ log(f"Message too large: {message_length} bytes (limit {MAX_MESSAGE_BYTES})")
61
+ raise ValueError(f"Message exceeds {MAX_MESSAGE_BYTES} byte limit")
62
+ raw_message = sys.stdin.buffer.read(message_length)
63
+ if len(raw_message) < message_length:
64
+ log(f"Expected {message_length} bytes, got {len(raw_message)}")
65
+ sys.exit(1)
66
+ return json.loads(raw_message.decode("utf-8"))
67
+
68
+
69
+ def send_message(message_obj):
70
+ """Send a single native messaging message to stdout."""
71
+ encoded = json.dumps(message_obj, separators=(",", ":")).encode("utf-8")
72
+ sys.stdout.buffer.write(struct.pack("@I", len(encoded)))
73
+ sys.stdout.buffer.write(encoded)
74
+ sys.stdout.buffer.flush()
75
+
76
+
77
+ def type_text(text, delay=0.05):
78
+ """
79
+ Simulate OS-level keystrokes for the given text using pynput.
80
+
81
+ Args:
82
+ text: The string to type.
83
+ delay: Seconds to wait between each keystroke.
84
+
85
+ Returns:
86
+ The number of characters typed.
87
+ """
88
+ from pynput.keyboard import Controller
89
+
90
+ keyboard = Controller()
91
+ count = 0
92
+ for char in text:
93
+ keyboard.type(char)
94
+ count += 1
95
+ if delay > 0:
96
+ time.sleep(delay)
97
+ return count
98
+
99
+
100
+ def click_at(x, y):
101
+ """
102
+ Simulate an OS-level left mouse click at the given screen coordinates.
103
+
104
+ Args:
105
+ x: Screen X coordinate (pixels from left edge).
106
+ y: Screen Y coordinate (pixels from top edge).
107
+ """
108
+ from pynput.mouse import Controller, Button
109
+
110
+ mouse = Controller()
111
+ mouse.position = (x, y)
112
+ time.sleep(0.05) # brief pause after moving to ensure position is set
113
+ mouse.click(Button.left, 1)
114
+
115
+
116
+ def _check_rate_limit(timestamps):
117
+ """Enforce a sliding-window rate limit. Returns True if the action is allowed."""
118
+ now = time.monotonic()
119
+ # Discard timestamps outside the window
120
+ while timestamps and timestamps[0] <= now - RATE_LIMIT_WINDOW:
121
+ timestamps.pop(0)
122
+ if len(timestamps) >= RATE_LIMIT_MAX:
123
+ return False
124
+ timestamps.append(now)
125
+ return True
126
+
127
+
128
+ def _start_idle_watchdog(last_activity_ref):
129
+ """Background thread that exits the process after IDLE_TIMEOUT_SECONDS of inactivity."""
130
+ def watchdog():
131
+ while True:
132
+ elapsed = time.monotonic() - last_activity_ref[0]
133
+ remaining = IDLE_TIMEOUT_SECONDS - elapsed
134
+ if remaining <= 0:
135
+ log(f"Idle timeout ({IDLE_TIMEOUT_SECONDS}s) — exiting")
136
+ os._exit(0) # hard exit from background thread
137
+ time.sleep(min(remaining, 10))
138
+ t = threading.Thread(target=watchdog, daemon=True)
139
+ t.start()
140
+
141
+
142
+ def main():
143
+ log("Native messaging host started")
144
+ log(f"Platform: {sys.platform}")
145
+ log(f"Safety limits: idle_timeout={IDLE_TIMEOUT_SECONDS}s, "
146
+ f"rate_limit={RATE_LIMIT_MAX}/{RATE_LIMIT_WINDOW}s, "
147
+ f"max_text={MAX_TEXT_LENGTH}, max_msg={MAX_MESSAGE_BYTES}B")
148
+
149
+ action_timestamps = [] # for rate limiting
150
+ # Mutable ref so the watchdog thread can see updates
151
+ last_activity = [time.monotonic()]
152
+ _start_idle_watchdog(last_activity)
153
+
154
+ while True:
155
+ try:
156
+ message = read_message()
157
+ except ValueError as e:
158
+ # Message size exceeded
159
+ send_message({"status": "error", "message": str(e)})
160
+ continue
161
+
162
+ last_activity[0] = time.monotonic()
163
+
164
+ try:
165
+ log(f"Received: {message}")
166
+
167
+ # --- Rate limit ---
168
+ if not _check_rate_limit(action_timestamps):
169
+ log("Rate limit exceeded")
170
+ send_message({"status": "error", "message": f"Rate limit exceeded ({RATE_LIMIT_MAX} actions per {RATE_LIMIT_WINDOW}s)"})
171
+ continue
172
+
173
+ # Determine action: explicit "action" field, or default to "type" if "text" present
174
+ action = message.get("action")
175
+ if action is None:
176
+ if "text" in message:
177
+ action = "type"
178
+ else:
179
+ send_message({"status": "error", "message": "Missing 'action' field"})
180
+ continue
181
+
182
+ if action == "type":
183
+ text = message.get("text")
184
+ if text is None:
185
+ send_message({"status": "error", "message": "Missing 'text' field"})
186
+ continue
187
+
188
+ if not isinstance(text, str):
189
+ send_message({"status": "error", "message": "'text' must be a string"})
190
+ continue
191
+
192
+ if len(text) > MAX_TEXT_LENGTH:
193
+ send_message({"status": "error", "message": f"Text too long ({len(text)} chars, limit {MAX_TEXT_LENGTH})"})
194
+ continue
195
+
196
+ if len(text) == 0:
197
+ send_message({"status": "ok", "typed": 0})
198
+ continue
199
+
200
+ delay = message.get("delay", 0.05)
201
+ typed = type_text(text, delay=delay)
202
+ log(f"Typed {typed} characters")
203
+ send_message({"status": "ok", "typed": typed})
204
+
205
+ elif action == "click":
206
+ x = message.get("x")
207
+ y = message.get("y")
208
+ if x is None or y is None:
209
+ send_message({"status": "error", "message": "Missing 'x' or 'y' for click"})
210
+ continue
211
+
212
+ x = int(x)
213
+ y = int(y)
214
+ click_at(x, y)
215
+ log(f"Clicked at ({x}, {y})")
216
+ send_message({"status": "ok", "action": "click", "x": x, "y": y})
217
+
218
+ else:
219
+ send_message({"status": "error", "message": f"Unknown action: {action}"})
220
+
221
+ except json.JSONDecodeError as e:
222
+ log(f"Invalid JSON: {e}")
223
+ send_message({"status": "error", "message": f"Invalid JSON: {e}"})
224
+ except Exception as e:
225
+ log(f"Error: {e}")
226
+ send_message({"status": "error", "message": str(e)})
227
+
228
+
229
+ if __name__ == "__main__":
230
+ main()
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Register (or unregister) the Chrome Native Messaging host manifest.
4
+
5
+ Usage:
6
+ keystroke-sender-register <extension-id>
7
+ keystroke-sender-register --unregister
8
+ """
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+
17
+ HOST_NAME = "com.propdream.keystroke_sender"
18
+
19
+
20
+ def _manifest_dir():
21
+ """Return the platform-specific directory for Chrome native messaging manifests."""
22
+ if sys.platform == "darwin":
23
+ return os.path.expanduser(
24
+ "~/Library/Application Support/Google/Chrome/NativeMessagingHosts"
25
+ )
26
+ elif sys.platform == "linux":
27
+ return os.path.expanduser(
28
+ "~/.config/google-chrome/NativeMessagingHosts"
29
+ )
30
+ elif sys.platform == "win32":
31
+ return os.path.join(
32
+ os.environ.get("LOCALAPPDATA", ""),
33
+ "Google", "Chrome", "User Data", "NativeMessagingHosts",
34
+ )
35
+ else:
36
+ sys.exit(f"Unsupported platform: {sys.platform}")
37
+
38
+
39
+ def _host_executable():
40
+ """Find the installed keystroke-sender console script."""
41
+ path = shutil.which("keystroke-sender")
42
+ if path is None:
43
+ sys.exit(
44
+ "Error: 'keystroke-sender' not found on PATH.\n"
45
+ "Install the package first: pip install keystroke-sender"
46
+ )
47
+ return os.path.abspath(path)
48
+
49
+
50
+ def register(extension_id):
51
+ """Write the native messaging host manifest and (on Windows) the registry key."""
52
+ manifest_dir = _manifest_dir()
53
+ os.makedirs(manifest_dir, exist_ok=True)
54
+
55
+ host_path = _host_executable()
56
+
57
+ # On Windows, Chrome expects a .bat or .exe — the pip console_script is an .exe
58
+ manifest = {
59
+ "name": HOST_NAME,
60
+ "description": "Simulates OS-level keystrokes for Chrome Form Filler",
61
+ "path": host_path,
62
+ "type": "stdio",
63
+ "allowed_origins": [f"chrome-extension://{extension_id}/"],
64
+ }
65
+
66
+ manifest_path = os.path.join(manifest_dir, f"{HOST_NAME}.json")
67
+ with open(manifest_path, "w") as f:
68
+ json.dump(manifest, f, indent=2)
69
+
70
+ print(f"Manifest written to: {manifest_path}")
71
+ print(f"Host executable: {host_path}")
72
+
73
+ # Windows requires a registry key pointing to the manifest
74
+ if sys.platform == "win32":
75
+ reg_key = rf"HKCU\Software\Google\Chrome\NativeMessagingHosts\{HOST_NAME}"
76
+ subprocess.run(
77
+ ["reg", "add", reg_key, "/ve", "/t", "REG_SZ", "/d", manifest_path, "/f"],
78
+ check=True,
79
+ )
80
+ print(f"Registry key: {reg_key}")
81
+
82
+ print()
83
+ print(f"Native messaging host '{HOST_NAME}' registered successfully.")
84
+
85
+ if sys.platform == "darwin":
86
+ print()
87
+ print("IMPORTANT: On macOS, grant Accessibility permission to your")
88
+ print("terminal/Python in System Preferences > Privacy & Security > Accessibility.")
89
+
90
+
91
+ def unregister():
92
+ """Remove the native messaging host manifest and (on Windows) the registry key."""
93
+ manifest_dir = _manifest_dir()
94
+ manifest_path = os.path.join(manifest_dir, f"{HOST_NAME}.json")
95
+
96
+ if os.path.exists(manifest_path):
97
+ os.remove(manifest_path)
98
+ print(f"Removed manifest: {manifest_path}")
99
+ else:
100
+ print(f"Manifest not found: {manifest_path}")
101
+
102
+ if sys.platform == "win32":
103
+ reg_key = rf"HKCU\Software\Google\Chrome\NativeMessagingHosts\{HOST_NAME}"
104
+ subprocess.run(
105
+ ["reg", "delete", reg_key, "/f"],
106
+ check=False, # OK if key doesn't exist
107
+ )
108
+ print(f"Removed registry key: {reg_key}")
109
+
110
+ print(f"Native messaging host '{HOST_NAME}' unregistered.")
111
+
112
+
113
+ def main():
114
+ parser = argparse.ArgumentParser(
115
+ description="Register the keystroke-sender Chrome Native Messaging host."
116
+ )
117
+ parser.add_argument(
118
+ "extension_id",
119
+ nargs="?",
120
+ help="Chrome extension ID (find it at chrome://extensions)",
121
+ )
122
+ parser.add_argument(
123
+ "--unregister",
124
+ action="store_true",
125
+ help="Remove the native messaging host registration",
126
+ )
127
+ args = parser.parse_args()
128
+
129
+ if args.unregister:
130
+ unregister()
131
+ elif args.extension_id:
132
+ register(args.extension_id)
133
+ else:
134
+ # Interactive prompt
135
+ ext_id = input("Enter your Chrome extension ID (found at chrome://extensions): ").strip()
136
+ if not ext_id:
137
+ sys.exit("Error: Extension ID cannot be empty.")
138
+ register(ext_id)
139
+
140
+
141
+ if __name__ == "__main__":
142
+ main()
@@ -0,0 +1,30 @@
1
+ @echo off
2
+ REM uninstall.bat — Remove the Chrome Native Messaging host on Windows
3
+ setlocal
4
+
5
+ set HOST_NAME=com.propdream.keystroke_sender
6
+ set TARGET_DIR=%LOCALAPPDATA%\Google\Chrome\User Data\NativeMessagingHosts
7
+ set SCRIPT_DIR=%~dp0
8
+
9
+ REM Remove manifest file
10
+ if exist "%TARGET_DIR%\%HOST_NAME%.json" (
11
+ del "%TARGET_DIR%\%HOST_NAME%.json"
12
+ echo Removed manifest: %TARGET_DIR%\%HOST_NAME%.json
13
+ ) else (
14
+ echo Manifest not found — nothing to remove.
15
+ )
16
+
17
+ REM Remove registry key
18
+ reg delete "HKCU\Software\Google\Chrome\NativeMessagingHosts\%HOST_NAME%" /f 2>nul
19
+ echo Removed registry key.
20
+
21
+ REM Remove wrapper batch file
22
+ if exist "%SCRIPT_DIR%keystroke_sender_wrapper.bat" (
23
+ del "%SCRIPT_DIR%keystroke_sender_wrapper.bat"
24
+ echo Removed wrapper script.
25
+ )
26
+
27
+ echo.
28
+ echo Native messaging host '%HOST_NAME%' uninstalled.
29
+
30
+ endlocal
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+ # uninstall.sh — Remove the Chrome Native Messaging host on macOS or Linux
3
+ set -e
4
+
5
+ HOST_NAME="com.propdream.keystroke_sender"
6
+
7
+ if [[ "$OSTYPE" == "darwin"* ]]; then
8
+ TARGET_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
9
+ elif [[ "$OSTYPE" == "linux"* ]]; then
10
+ TARGET_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
11
+ else
12
+ echo "Unsupported OS: $OSTYPE"
13
+ exit 1
14
+ fi
15
+
16
+ MANIFEST="$TARGET_DIR/$HOST_NAME.json"
17
+
18
+ if [ -f "$MANIFEST" ]; then
19
+ rm -f "$MANIFEST"
20
+ echo "Native messaging host '$HOST_NAME' uninstalled."
21
+ echo " Removed: $MANIFEST"
22
+ else
23
+ echo "Manifest not found at $MANIFEST — nothing to remove."
24
+ fi