3253828fef
Add complete navegator system for stealthy browser automation: - CDP client with WebSocket communication - Browser API with navigation, storage, network, runtime - Stealth flags and anti-detection scripts - Persistent profile support - Examples and comprehensive documentation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
397 lines
11 KiB
Go
397 lines
11 KiB
Go
package browser
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// EvaluateResult representa el resultado de una evaluación de JavaScript.
|
|
type EvaluateResult struct {
|
|
Type string `json:"type"`
|
|
Value interface{} `json:"value"`
|
|
Description string `json:"description"`
|
|
ObjectID string `json:"objectId,omitempty"`
|
|
SubType string `json:"subtype,omitempty"`
|
|
RawResult json.RawMessage `json:"-"`
|
|
}
|
|
|
|
// Evaluate ejecuta código JavaScript en el contexto de la página.
|
|
func (b *Browser) Evaluate(ctx context.Context, expression string) (*EvaluateResult, error) {
|
|
params := map[string]interface{}{
|
|
"expression": expression,
|
|
"returnByValue": true,
|
|
"awaitPromise": true,
|
|
"userGesture": true,
|
|
}
|
|
|
|
var response struct {
|
|
Result struct {
|
|
Type string `json:"type"`
|
|
Value interface{} `json:"value"`
|
|
Description string `json:"description"`
|
|
ObjectID string `json:"objectId"`
|
|
SubType string `json:"subtype"`
|
|
} `json:"result"`
|
|
ExceptionDetails *struct {
|
|
Text string `json:"text"`
|
|
Exception struct {
|
|
Description string `json:"description"`
|
|
} `json:"exception"`
|
|
} `json:"exceptionDetails"`
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "Runtime.evaluate", params, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to evaluate: %w", err)
|
|
}
|
|
|
|
if response.ExceptionDetails != nil {
|
|
return nil, fmt.Errorf("JavaScript exception: %s - %s",
|
|
response.ExceptionDetails.Text,
|
|
response.ExceptionDetails.Exception.Description)
|
|
}
|
|
|
|
result := &EvaluateResult{
|
|
Type: response.Result.Type,
|
|
Value: response.Result.Value,
|
|
Description: response.Result.Description,
|
|
ObjectID: response.Result.ObjectID,
|
|
SubType: response.Result.SubType,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// EvaluateOnNode ejecuta JavaScript en el contexto de un nodo específico.
|
|
func (b *Browser) EvaluateOnNode(ctx context.Context, nodeID int64, expression string) (*EvaluateResult, error) {
|
|
// Primero obtener el objectId del nodo
|
|
var objResult struct {
|
|
Object struct {
|
|
ObjectID string `json:"objectId"`
|
|
} `json:"object"`
|
|
}
|
|
|
|
params := map[string]interface{}{
|
|
"nodeId": nodeID,
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "DOM.resolveNode", params, &objResult); err != nil {
|
|
return nil, fmt.Errorf("failed to resolve node: %w", err)
|
|
}
|
|
|
|
// Ejecutar función en el objeto
|
|
callParams := map[string]interface{}{
|
|
"functionDeclaration": fmt.Sprintf("function() { return (%s); }", expression),
|
|
"objectId": objResult.Object.ObjectID,
|
|
"returnByValue": true,
|
|
"awaitPromise": true,
|
|
}
|
|
|
|
var response struct {
|
|
Result struct {
|
|
Type string `json:"type"`
|
|
Value interface{} `json:"value"`
|
|
Description string `json:"description"`
|
|
} `json:"result"`
|
|
ExceptionDetails *struct {
|
|
Text string `json:"text"`
|
|
} `json:"exceptionDetails"`
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "Runtime.callFunctionOn", callParams, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to call function: %w", err)
|
|
}
|
|
|
|
if response.ExceptionDetails != nil {
|
|
return nil, fmt.Errorf("JavaScript exception: %s", response.ExceptionDetails.Text)
|
|
}
|
|
|
|
return &EvaluateResult{
|
|
Type: response.Result.Type,
|
|
Value: response.Result.Value,
|
|
Description: response.Result.Description,
|
|
}, nil
|
|
}
|
|
|
|
// EvaluateAsync ejecuta JavaScript de forma asíncrona (retorna Promise).
|
|
func (b *Browser) EvaluateAsync(ctx context.Context, expression string) (*EvaluateResult, error) {
|
|
params := map[string]interface{}{
|
|
"expression": expression,
|
|
"returnByValue": true,
|
|
"awaitPromise": true,
|
|
"userGesture": true,
|
|
}
|
|
|
|
var response struct {
|
|
Result struct {
|
|
Type string `json:"type"`
|
|
Value interface{} `json:"value"`
|
|
Description string `json:"description"`
|
|
ObjectID string `json:"objectId"`
|
|
} `json:"result"`
|
|
ExceptionDetails *struct {
|
|
Text string `json:"text"`
|
|
Exception struct {
|
|
Description string `json:"description"`
|
|
} `json:"exception"`
|
|
} `json:"exceptionDetails"`
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "Runtime.evaluate", params, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to evaluate async: %w", err)
|
|
}
|
|
|
|
if response.ExceptionDetails != nil {
|
|
return nil, fmt.Errorf("JavaScript exception: %s - %s",
|
|
response.ExceptionDetails.Text,
|
|
response.ExceptionDetails.Exception.Description)
|
|
}
|
|
|
|
return &EvaluateResult{
|
|
Type: response.Result.Type,
|
|
Value: response.Result.Value,
|
|
Description: response.Result.Description,
|
|
ObjectID: response.Result.ObjectID,
|
|
}, nil
|
|
}
|
|
|
|
// CallFunction ejecuta una función JavaScript con argumentos.
|
|
func (b *Browser) CallFunction(ctx context.Context, functionDeclaration string, args ...interface{}) (*EvaluateResult, error) {
|
|
// Convertir args a formato CDP
|
|
cdpArgs := make([]map[string]interface{}, len(args))
|
|
for i, arg := range args {
|
|
cdpArgs[i] = map[string]interface{}{
|
|
"value": arg,
|
|
}
|
|
}
|
|
|
|
params := map[string]interface{}{
|
|
"functionDeclaration": functionDeclaration,
|
|
"arguments": cdpArgs,
|
|
"returnByValue": true,
|
|
"awaitPromise": true,
|
|
"userGesture": true,
|
|
}
|
|
|
|
var response struct {
|
|
Result struct {
|
|
Type string `json:"type"`
|
|
Value interface{} `json:"value"`
|
|
Description string `json:"description"`
|
|
} `json:"result"`
|
|
ExceptionDetails *struct {
|
|
Text string `json:"text"`
|
|
} `json:"exceptionDetails"`
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "Runtime.callFunctionOn", params, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to call function: %w", err)
|
|
}
|
|
|
|
if response.ExceptionDetails != nil {
|
|
return nil, fmt.Errorf("JavaScript exception: %s", response.ExceptionDetails.Text)
|
|
}
|
|
|
|
return &EvaluateResult{
|
|
Type: response.Result.Type,
|
|
Value: response.Result.Value,
|
|
Description: response.Result.Description,
|
|
}, nil
|
|
}
|
|
|
|
// GetProperty obtiene una propiedad de un objeto.
|
|
func (b *Browser) GetProperty(ctx context.Context, objectID string, propertyName string) (*EvaluateResult, error) {
|
|
params := map[string]interface{}{
|
|
"objectId": objectID,
|
|
}
|
|
|
|
var response struct {
|
|
Result []struct {
|
|
Name string `json:"name"`
|
|
Value struct {
|
|
Type string `json:"type"`
|
|
Value interface{} `json:"value"`
|
|
Description string `json:"description"`
|
|
} `json:"value"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "Runtime.getProperties", params, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to get properties: %w", err)
|
|
}
|
|
|
|
for _, prop := range response.Result {
|
|
if prop.Name == propertyName {
|
|
return &EvaluateResult{
|
|
Type: prop.Value.Type,
|
|
Value: prop.Value.Value,
|
|
Description: prop.Value.Description,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("property not found: %s", propertyName)
|
|
}
|
|
|
|
// AddBinding agrega un binding (función JS que llama a Go).
|
|
type BindingCallback func(args []interface{}) interface{}
|
|
|
|
// AddBinding expone una función Go al contexto JavaScript.
|
|
func (b *Browser) AddBinding(ctx context.Context, name string, callback BindingCallback) error {
|
|
// Agregar binding en Runtime
|
|
params := map[string]interface{}{
|
|
"name": name,
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "Runtime.addBinding", params, nil); err != nil {
|
|
return fmt.Errorf("failed to add binding: %w", err)
|
|
}
|
|
|
|
// Registrar evento para manejar llamadas
|
|
b.cdpClient.On("Runtime.bindingCalled", func(eventParams json.RawMessage) {
|
|
var event struct {
|
|
Name string `json:"name"`
|
|
Payload string `json:"payload"`
|
|
ExecutionContextID int64 `json:"executionContextId"`
|
|
}
|
|
|
|
if err := json.Unmarshal(eventParams, &event); err != nil {
|
|
return
|
|
}
|
|
|
|
if event.Name != name {
|
|
return
|
|
}
|
|
|
|
// Parsear args
|
|
var args []interface{}
|
|
if err := json.Unmarshal([]byte(event.Payload), &args); err != nil {
|
|
return
|
|
}
|
|
|
|
// Ejecutar callback
|
|
result := callback(args)
|
|
|
|
// Devolver resultado (evaluando código que lo retorna)
|
|
returnScript := fmt.Sprintf("window.%s_result = %v", name, result)
|
|
b.Evaluate(ctx, returnScript)
|
|
})
|
|
|
|
// Inyectar wrapper en JavaScript
|
|
wrapperScript := fmt.Sprintf(`
|
|
window.%s = async (...args) => {
|
|
const payload = JSON.stringify(args);
|
|
window.%s_result = undefined;
|
|
await window.chrome.runtime.sendMessage({
|
|
type: 'binding',
|
|
name: '%s',
|
|
payload: payload
|
|
});
|
|
// Esperar resultado (polling simple)
|
|
while (window.%s_result === undefined) {
|
|
await new Promise(r => setTimeout(r, 10));
|
|
}
|
|
return window.%s_result;
|
|
};
|
|
`, name, name, name, name, name)
|
|
|
|
_, err := b.Evaluate(ctx, wrapperScript)
|
|
return err
|
|
}
|
|
|
|
// ConsoleMessage representa un mensaje de consola.
|
|
type ConsoleMessage struct {
|
|
Type string `json:"type"`
|
|
Args []interface{} `json:"args"`
|
|
Text string `json:"text"`
|
|
URL string `json:"url"`
|
|
Line int `json:"lineNumber"`
|
|
Column int `json:"columnNumber"`
|
|
}
|
|
|
|
// OnConsole registra un handler para mensajes de consola.
|
|
func (b *Browser) OnConsole(handler func(msg *ConsoleMessage)) {
|
|
b.cdpClient.On("Runtime.consoleAPICalled", func(params json.RawMessage) {
|
|
var event struct {
|
|
Type string `json:"type"`
|
|
Args []struct {
|
|
Type string `json:"type"`
|
|
Value interface{} `json:"value"`
|
|
} `json:"args"`
|
|
StackTrace struct {
|
|
CallFrames []struct {
|
|
URL string `json:"url"`
|
|
LineNumber int `json:"lineNumber"`
|
|
ColumnNumber int `json:"columnNumber"`
|
|
} `json:"callFrames"`
|
|
} `json:"stackTrace"`
|
|
}
|
|
|
|
if err := json.Unmarshal(params, &event); err != nil {
|
|
return
|
|
}
|
|
|
|
msg := &ConsoleMessage{
|
|
Type: event.Type,
|
|
Args: make([]interface{}, len(event.Args)),
|
|
}
|
|
|
|
// Construir texto del mensaje
|
|
text := ""
|
|
for i, arg := range event.Args {
|
|
msg.Args[i] = arg.Value
|
|
if i > 0 {
|
|
text += " "
|
|
}
|
|
text += fmt.Sprintf("%v", arg.Value)
|
|
}
|
|
msg.Text = text
|
|
|
|
// Agregar info de stack trace si existe
|
|
if len(event.StackTrace.CallFrames) > 0 {
|
|
frame := event.StackTrace.CallFrames[0]
|
|
msg.URL = frame.URL
|
|
msg.Line = frame.LineNumber
|
|
msg.Column = frame.ColumnNumber
|
|
}
|
|
|
|
handler(msg)
|
|
})
|
|
}
|
|
|
|
// EnableConsole habilita eventos de consola.
|
|
func (b *Browser) EnableConsole(ctx context.Context) error {
|
|
return b.cdpClient.Execute(ctx, "Runtime.enable", nil, nil)
|
|
}
|
|
|
|
// QuerySelector helper para ejecutar querySelector desde JavaScript.
|
|
func (b *Browser) QuerySelector(ctx context.Context, selector string) (*EvaluateResult, error) {
|
|
script := fmt.Sprintf(`document.querySelector('%s')`, selector)
|
|
return b.Evaluate(ctx, script)
|
|
}
|
|
|
|
// QuerySelectorAll ejecuta querySelectorAll y retorna array de elementos.
|
|
func (b *Browser) QuerySelectorAll(ctx context.Context, selector string) (*EvaluateResult, error) {
|
|
script := fmt.Sprintf(`Array.from(document.querySelectorAll('%s'))`, selector)
|
|
return b.Evaluate(ctx, script)
|
|
}
|
|
|
|
// WaitForFunction espera a que una función JavaScript retorne true.
|
|
func (b *Browser) WaitForFunction(ctx context.Context, function string, pollInterval int) error {
|
|
script := fmt.Sprintf(`
|
|
new Promise((resolve) => {
|
|
const check = () => {
|
|
if (%s) {
|
|
resolve(true);
|
|
} else {
|
|
setTimeout(check, %d);
|
|
}
|
|
};
|
|
check();
|
|
})
|
|
`, function, pollInterval)
|
|
|
|
_, err := b.EvaluateAsync(ctx, script)
|
|
return err
|
|
}
|