snapkitty
developer-tools
c++
File size: 3,989 Bytes
0196580
 
 
7074ec9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
% SPDX-License-Identifier: LicenseRef-Sovereign-Leviathan-SL-AGPL3-001
% Licensing: see LICENSE in the repository root.

%%%-------------------------------------------------------------------
%%% @doc HK-BROWSER :: Chrome's `/json' HTTP control endpoint.
%%%
%%% Chromium, started with `--remote-debugging-port', exposes a
%%% small plain-HTTP API on that port for target (tab) lifecycle --
%%% separate from the per-target CDP WebSocket that `hk_cdp_client'
%%% speaks. This module wraps that HTTP surface with `httpc' (part
%%% of `inets', already a runtime dependency) so `hk_browser_session'
%%% doesn't need its own HTTP client for this one-off, low-volume
%%% traffic.
%%% @end
%%%-------------------------------------------------------------------
-module(hk_chrome_http).

-export([wait_for_ready/2, new_target/2, close_target/2, version/1]).

-define(POLL_INTERVAL_MS, 100).

%% @doc Poll `/json/version' until Chrome's debug HTTP server answers
%% or `TimeoutMs' elapses. Used right after launching the Chrome OS
%% process, whose startup time is not instantaneous or guaranteed.
-spec wait_for_ready(inet:port_number(), timeout()) -> ok | {error, timeout}.
wait_for_ready(Port, TimeoutMs) ->
    Deadline = erlang:monotonic_time(millisecond) + TimeoutMs,
    wait_loop(Port, Deadline).

wait_loop(Port, Deadline) ->
    case version(Port) of
        {ok, _} -> ok;
        {error, _} ->
            case erlang:monotonic_time(millisecond) >= Deadline of
                true -> {error, timeout};
                false ->
                    timer:sleep(?POLL_INTERVAL_MS),
                    wait_loop(Port, Deadline)
            end
    end.

-spec version(inet:port_number()) -> {ok, map()} | {error, term()}.
version(Port) ->
    get_json("http://127.0.0.1:" ++ integer_to_list(Port) ++ "/json/version").

%% @doc Create a new target (tab) navigated to `Url'; returns its
%% CDP target id and WebSocket debugger URL.
-spec new_target(inet:port_number(), binary()) ->
    {ok, #{target_id => binary(), ws_url => binary()}} | {error, term()}.
new_target(Port, Url) ->
    Encoded = uri_string:quote(binary_to_list(Url)),
    TargetUrl = "http://127.0.0.1:" ++ integer_to_list(Port) ++ "/json/new?" ++ Encoded,
    %% Chrome >= 111 rejects GET on /json/new with 405 Method Not
    %% Allowed (a CSRF-style hardening: creating a target must not be
    %% triggerable by a plain cross-origin GET/link) and requires PUT
    %% instead. `httpc:request/4' (not the shorthand `/1') is needed to
    %% pass a method other than the default.
    case httpc:request(put, {TargetUrl, [], "text/plain", <<>>}, [{timeout, 5000}], []) of
        {ok, {{_, 200, _}, _, Body}} ->
            case (catch jsx:decode(list_to_binary(Body), [return_maps])) of
                #{<<"id">> := TargetId, <<"webSocketDebuggerUrl">> := WsUrl} ->
                    {ok, #{target_id => TargetId, ws_url => WsUrl}};
                Other ->
                    {error, {unexpected_response, Other}}
            end;
        {ok, {{_, Status, _}, _, _}} ->
            {error, {http_status, Status}};
        {error, Reason} ->
            {error, Reason}
    end.

-spec close_target(inet:port_number(), binary()) -> ok | {error, term()}.
close_target(Port, TargetId) ->
    Url = "http://127.0.0.1:" ++ integer_to_list(Port) ++ "/json/close/"
        ++ binary_to_list(TargetId),
    case httpc:request(get, {Url, []}, [], []) of
        {ok, {{_, 200, _}, _, _}} -> ok;
        {ok, {{_, Status, _}, _, _}} -> {error, {http_status, Status}};
        {error, Reason} -> {error, Reason}
    end.

%%% internal

get_json(Url) ->
    case httpc:request(get, {Url, []}, [{timeout, 5000}], []) of
        {ok, {{_, 200, _}, _, Body}} ->
            try {ok, jsx:decode(list_to_binary(Body), [return_maps])}
            catch _:_ -> {error, invalid_json} end;
        {ok, {{_, Status, _}, _, _}} ->
            {error, {http_status, Status}};
        {error, Reason} ->
            {error, Reason}
    end.