Skip to content

Commit

Permalink
Adding files
Browse files Browse the repository at this point in the history
  • Loading branch information
G33kDude committed Jan 16, 2018
1 parent 5d2c55d commit d2fb84b
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ChromeProfile
7 changes: 7 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[submodule "lib/WebSocket.ahk"]
path = lib/WebSocket.ahk
url = https://github.com/G33kDude/WebSocket.ahk.git
[submodule "lib/AutoHotkey-JSON"]
path = lib/AutoHotkey-JSON
url = https://github.com/G33kDude/AutoHotkey-JSON.git
branch = boolean
176 changes: 176 additions & 0 deletions Chrome.ahk
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
class Chrome
{
static DebugPort := 9222

; Escape a string in a manner suitable for command line parameters
CliEscape(Param)
{
return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """"
}

__New(ProfilePath:="", URL:="about:blank", ChromePath:="", DebugPort:="")
{
if (ProfilePath != "" && !InStr(FileExist(ProfilePath), "D"))
throw Exception("The given ProfilePath does not exist")
this.ProfilePath := ProfilePath

; TODO: Perform a more rigorous search for Chrome
if (ChromePath == "")
FileGetShortcut, %A_StartMenuCommon%\Programs\Google Chrome.lnk, ChromePath
if !FileExist(ChromePath)
throw Exception("Chrome could not be found")
this.ChromePath := ChromePath

if (DebugPort != "")
{
this.DebugPort := Round(DebugPort)
if (this.DebugPort <= 0) ; TODO: Support DebugPort of 0
throw Exception("DebugPort must be a positive integer")
}

; TODO: Support an array of URLs
Run, % this.CliEscape(ChromePath)
. " --remote-debugging-port=" this.DebugPort
. (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "")
. (URL ? " " this.CliEscape(URL) : "")
}

GetTabs()
{
http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
http.open("GET", "http://127.0.0.1:" this.DebugPort "/json")
http.send()
return this.Jxon_Load(http.responseText)
}

GetTab(Index:=0)
{
; TODO: Filter pages by type before returning an indexed page
if (Index > 0)
return new this.Tab(this.GetTabs()[Index])

for Index, Tab in this.GetTabs()
if (Tab.type == "page")
return new this.Tab(Tab)
}

class Tab
{
Connected := False
ID := 0
Responses := []

__New(wsurl)
{
this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False)

; TODO: Throw exception on invalid objects
if IsObject(wsurl)
wsurl := wsurl.webSocketDebuggerUrl

wsurl := StrReplace(wsurl, "localhost", "127.0.0.1")
this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this}
this.ws.__New(wsurl)

while !this.Connected
Sleep, 50
}

Call(DomainAndMethod, Params:="", WaitForResponse:=True)
{
if !this.Connected
throw Exception("Not connected to tab")

; Use a temporary variable for ID in case more calls are made
; before we receive a response.
ID := this.ID += 1
this.ws.Send(Chrome.Jxon_Dump({"id": ID
, "method": DomainAndMethod, "params": Params}))

if !WaitForResponse
return

; Wait for the response
this.responses[ID] := False
while !this.responses[ID]
Sleep, 50

; Get the response, check if it's an error
response := this.responses.Delete(ID)
if (response.error)
throw Exception("Chrome indicated error in response",, Chrome.Jxon_Dump(response.error))

return response.result
}

Evaluate(JS)
{
response := this.Call("Runtime.evaluate",
( LTrim Join
{
"expression": JS,
"objectGroup": "console",
"includeCommandLineAPI": Chrome.Jxon_True(),
"silent": Chrome.Jxon_False(),
"returnByValue": Chrome.Jxon_False(),
"userGesture": Chrome.Jxon_True(),
"awaitPromise": Chrome.Jxon_False()
}
))

if (response.exceptionDetails)
throw Exception(response.result.description,, Chrome.Jxon_Dump(response.exceptionDetails))

return response.result
}

WaitForLoad(DesiredState:="complete", Interval:=100)
{
while this.Evaluate("document.readyState").value != DesiredState
Sleep, %Interval%
}

Event(EventName, Event)
{
; Called from WebSocket
if this.Parent
this := this.Parent

; TODO: Handle Error events
if (EventName == "Open")
{
this.Connected := True
BoundKeepAlive := this.BoundKeepAlive
SetTimer, %BoundKeepAlive%, 15000
}
else if (EventName == "Message")
{
data := Chrome.Jxon_Load(Event.data)
if this.responses.HasKey(data.ID)
this.responses[data.ID] := data
}
else if (EventName == "Close")
{
this.Disconnect()
}
}

Disconnect()
{
if !this.Connected
return

this.Connected := False
this.ws.Delete("Parent")
this.ws.Disconnect()

BoundKeepAlive := this.BoundKeepAlive
SetTimer, %BoundKeepAlive%, Delete
this.Delete("BoundKeepAlive")
}

#Include %A_LineFile%\..\lib\WebSocket.ahk\WebSocket.ahk
}

#Include %A_LineFile%\..\lib\AutoHotkey-JSON\Jxon.ahk
}
51 changes: 51 additions & 0 deletions Examples/InjectJS.ahk
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#NoEnv
SetBatchLines, -1


; --- Create a new chrome instance ---

FileCreateDir, ChromeProfile
ChromeInst := new Chrome("ChromeProfile", "https://autohotkey.com/")


; --- Connect to the active tab ---

Tab := ChromeInst.GetTab()


; --- Perform JavaScript injection ---

Loop
{
InputBox, JS,,
( LTrim
Enter some JavaScript to be run in the tab

For example:
alert('hi');
window.location = "https://p.ahkscript.org/";
)

if ErrorLevel
break

try
Result := Tab.Evaluate(JS)
catch e
{
MsgBox, % "Exception encountered in " e.What ":`n`n"
. e.Message "`n`n"
. "Specifically:`n`n"
. Chrome.Jxon_Dump(Chrome.Jxon_Load(e.Extra), "`t")

continue
}

MsgBox, % "Result: " Chrome.Jxon_Dump(Result)
}

ExitApp
return


#include ../Chrome.ahk
49 changes: 49 additions & 0 deletions Examples/Pastebin.ahk
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#NoEnv
SetBatchLines, -1


; --- Create a new chrome instance ---

FileCreateDir, ChromeProfile
ChromeInst := new Chrome("ChromeProfile")


; --- Connect to the active tab ---

Tab := ChromeInst.GetTab()


; --- Navigate to the pastebin ---

Tab.Call("Page.navigate", {"url": "https://p.ahkscript.org"})
Tab.WaitForLoad()


; --- Manipulation via DOM ---

; Find the root node
RootNode := Tab.Call("DOM.getDocument").root

; Find and change the name element
NameNode := Tab.Call("DOM.querySelector", {"nodeId": RootNode.nodeId, "selector": "input[name=name]"})
Tab.Call("DOM.setAttributeValue", {"nodeId": NameNode.NodeId, "name": "value", "value": "ChromeBot"})

; Find and change the description element
DescNode := Tab.Call("DOM.querySelector", {"nodeId": RootNode.nodeId, "selector": "input[name=desc]"})
Tab.Call("DOM.setAttributeValue", {"nodeId": DescNode.NodeId, "name": "value", "value": "Pasted with ChromeBot"})


; --- Manipulation via JavaScript ---

Tab.Evaluate("editor.setValue('test');")
Tab.Evaluate("document.querySelector('input[type=submit]').click();")
Tab.WaitForLoad()

MsgBox, % Tab.Evaluate("window.location.href").value

Tab.Call("Browser.close")
ExitApp
return


#include ../Chrome.ahk
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,73 @@
# Chrome.ahk
# Chrome.ahk

Automate Google Chrome using native AutoHotkey.


## How it works

Chrome offers a WebSocket based API they call the **Chrome DevTools Protocol**. This API is what allows web development tools to build integrations, and tools such as Selenium to perform their automation. The protocol's documentation describes a plethora of exciting endpoints accessible using this library, and can be found at the link below.

https://chromedevtools.github.io/devtools-protocol/


## Advantages

* **No external dependencies such as Selenium are required**
* Chrome can be automated even when running in headless mode
* Launching in headless mode is not currently supported by this library
* Chrome consistently benchmarks better than Internet Explorer
* Chrome offers extensions which provide unique opportunities for interaction
* Automate your Chromecast
* Connect to remote servers with FoxyProxy and update web based configs
* Manage your password vault with LastPass
* Many features are available that would be difficult to replicate in Internet Explorer
* `Page.printToPDF`
* `Page.captureScreenshot`
* Geolocation spoofing


## Limitations

* Chrome **must** be started in debug mode
* If chrome is already running out of debug mode, it must either be **closed and reopened** or **launched again under a new profile** that isn't already running
* **You cannot attach to an existing non-debug session**
* Less flexible than Internet Explorer's COM interface
* Cannot pass function references for callbacks


## Using this Library

To start using this library you need to create an instance of the class `Chrome`. `Chrome`'s constructor accepts four optional parameters:

1. **ProfilePath** - This is the path, relative to the working directory, that your Chrome user profile is located. If an empty folder is given, chrome will generate a new user profile in it. **When this parameter is omitted, Chrome will be launched under the default user profile.** However, if chrome is already running under that user profile out of debug mode, this will fail. Because of this, **it is recommended to always launch Chrome under an alternate user profile.**
2. **URL** - The page that chrome should initially be opened to. Pass an empty string to open Chrome's homepage. **When this parameter is omitted, Chrome will be opened to `about:blank`.**
3. **ChromePath** - The path to find the Chrome executable file. **When this parameter is omitted, Chrome will be launched from the path in its start menu entry.**
4. **DebugPort** - The network port to communicate with Chrome over. **When this parameter is omitted, port `9222` will be used** as specified in the Chrome DevTools Protocol documentation.

Once an instance of the class `Chrome` has been created, Google Chrome will be launched. To connect to the newly opened page call `TabInstance := ChromeInstance.GetTab()`. Afterward, use `Tab.Call()` to call protocol endpoints, and `Tab.Evaluate()` to execute JavaScript.

```AutoHotkey
#Include Chrome.ahk
; Create an instance of the Chrome class using
; the folder ChromeProfile to store the user profile
FileCreateDir, ChromeProfile
ChromeInst := new Chrome("ChromeProfile")
; Connect to the newly opened tab and navigate to another website
; Note: If your first action is to navigate away, it may be just as
; effective to provide the target URL when instantiating the Chrome class
Tab := ChromeInst.GetTab()
Tab.Call("Page.navigate", {"url": "https://autohotkey.com/"})
Tab.WaitForLoad()
; Execute some JavaScript
Tab.Evaluate("alert('Hello World!');")
; Close the browser (note: this closes *all* pages/tabs)
Tab.Call("Browser.close")
ExitApp
return
```

**You can find more sample code showing how to use this library in the Examples folder.**
1 change: 1 addition & 0 deletions lib/AutoHotkey-JSON
Submodule AutoHotkey-JSON added at 189957
1 change: 1 addition & 0 deletions lib/WebSocket.ahk
Submodule WebSocket.ahk added at 8a0c1f

0 comments on commit d2fb84b

Please sign in to comment.