1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-10 09:58:01 +00:00
putty-source/proxy/proxy.c
Simon Tatham a2ff884512 Richer data type for interactive prompt results.
All the seat functions that request an interactive prompt of some kind
to the user - both the main seat_get_userpass_input and the various
confirmation dialogs for things like host keys - were using a simple
int return value, with the general semantics of 0 = "fail", 1 =
"proceed" (and in the case of seat_get_userpass_input, answers to the
prompts were provided), and -1 = "request in progress, wait for a
callback".

In this commit I change all those functions' return types to a new
struct called SeatPromptResult, whose primary field is an enum
replacing those simple integer values.

The main purpose is that the enum has not three but _four_ values: the
"fail" result has been split into 'user abort' and 'software abort'.
The distinction is that a user abort occurs as a result of an
interactive UI action, such as the user clicking 'cancel' in a dialog
box or hitting ^D or ^C at a terminal password prompt - and therefore,
there's no need to display an error message telling the user that the
interactive operation has failed, because the user already knows,
because they _did_ it. 'Software abort' is from any other cause, where
PuTTY is the first to know there was a problem, and has to tell the
user.

We already had this 'user abort' vs 'software abort' distinction in
other parts of the code - the SSH backend has separate termination
functions which protocol layers can call. But we assumed that any
failure from an interactive prompt request fell into the 'user abort'
category, which is not true. A couple of examples: if you configure a
host key fingerprint in your saved session via the SSH > Host keys
pane, and the server presents a host key that doesn't match it, then
verify_ssh_host_key would report that the user had aborted the
connection, and feel no need to tell the user what had gone wrong!
Similarly, if a password provided on the command line was not
accepted, then (after I fixed the semantics of that in the previous
commit) the same wrong handling would occur.

So now, those Seat prompt functions too can communicate whether the
user or the software originated a connection abort. And in the latter
case, we also provide an error message to present to the user. Result:
in those two example cases (and others), error messages should no
longer go missing.

Implementation note: to avoid the hassle of having the error message
in a SeatPromptResult being a dynamically allocated string (and hence,
every recipient of one must always check whether it's non-NULL and
free it on every exit path, plus being careful about copying the
struct around), I've instead arranged that the structure contains a
function pointer and a couple of parameters, so that the string form
of the message can be constructed on demand. That way, the only users
who need to free it are the ones who actually _asked_ for it in the
first place, which is a much smaller set.

(This is one of the rare occasions that I regret not having C++'s
extra features available in this code base - a unique_ptr or
shared_ptr to a string would have been just the thing here, and the
compiler would have done all the hard work for me of remembering where
to insert the frees!)
2021-12-28 18:08:31 +00:00

631 lines
20 KiB
C

/*
* Network proxy abstraction in PuTTY
*
* A proxy layer, if necessary, wedges itself between the network
* code and the higher level backend.
*/
#include <assert.h>
#include <ctype.h>
#include <string.h>
#include "putty.h"
#include "network.h"
#include "proxy.h"
#define do_proxy_dns(conf) \
(conf_get_int(conf, CONF_proxy_dns) == FORCE_ON || \
(conf_get_int(conf, CONF_proxy_dns) == AUTO && \
conf_get_int(conf, CONF_proxy_type) != PROXY_SOCKS4))
static void proxy_negotiator_cleanup(ProxySocket *ps)
{
if (ps->pn) {
proxy_negotiator_free(ps->pn);
ps->pn = NULL;
}
if (ps->clientseat) {
interactor_return_seat(ps->clientitr);
ps->clientitr = NULL;
ps->clientseat = NULL;
}
}
/*
* Call this when proxy negotiation is complete, so that this
* socket can begin working normally.
*/
void proxy_activate(ProxySocket *ps)
{
size_t output_before, output_after;
proxy_negotiator_cleanup(ps);
plug_log(ps->plug, PLUGLOG_CONNECT_SUCCESS, NULL, 0, NULL, 0);
/* we want to ignore new receive events until we have sent
* all of our buffered receive data.
*/
sk_set_frozen(ps->sub_socket, true);
/* how many bytes of output have we buffered? */
output_before = bufchain_size(&ps->pending_oob_output_data) +
bufchain_size(&ps->pending_output_data);
/* and keep track of how many bytes do not get sent. */
output_after = 0;
/* send buffered OOB writes */
while (bufchain_size(&ps->pending_oob_output_data) > 0) {
ptrlen data = bufchain_prefix(&ps->pending_oob_output_data);
output_after += sk_write_oob(ps->sub_socket, data.ptr, data.len);
bufchain_consume(&ps->pending_oob_output_data, data.len);
}
/* send buffered normal writes */
while (bufchain_size(&ps->pending_output_data) > 0) {
ptrlen data = bufchain_prefix(&ps->pending_output_data);
output_after += sk_write(ps->sub_socket, data.ptr, data.len);
bufchain_consume(&ps->pending_output_data, data.len);
}
/* if we managed to send any data, let the higher levels know. */
if (output_after < output_before)
plug_sent(ps->plug, output_after);
/* if we have a pending EOF to send, send it */
if (ps->pending_eof) sk_write_eof(ps->sub_socket);
/* if the backend wanted the socket unfrozen, try to unfreeze.
* our set_frozen handler will flush buffered receive data before
* unfreezing the actual underlying socket.
*/
if (!ps->freeze)
sk_set_frozen(&ps->sock, false);
}
/* basic proxy socket functions */
static Plug *sk_proxy_plug (Socket *s, Plug *p)
{
ProxySocket *ps = container_of(s, ProxySocket, sock);
Plug *ret = ps->plug;
if (p)
ps->plug = p;
return ret;
}
static void sk_proxy_close (Socket *s)
{
ProxySocket *ps = container_of(s, ProxySocket, sock);
sk_close(ps->sub_socket);
sk_addr_free(ps->remote_addr);
proxy_negotiator_cleanup(ps);
bufchain_clear(&ps->output_from_negotiator);
sfree(ps);
}
static size_t sk_proxy_write (Socket *s, const void *data, size_t len)
{
ProxySocket *ps = container_of(s, ProxySocket, sock);
if (ps->pn) {
bufchain_add(&ps->pending_output_data, data, len);
return bufchain_size(&ps->pending_output_data);
}
return sk_write(ps->sub_socket, data, len);
}
static size_t sk_proxy_write_oob (Socket *s, const void *data, size_t len)
{
ProxySocket *ps = container_of(s, ProxySocket, sock);
if (ps->pn) {
bufchain_clear(&ps->pending_output_data);
bufchain_clear(&ps->pending_oob_output_data);
bufchain_add(&ps->pending_oob_output_data, data, len);
return len;
}
return sk_write_oob(ps->sub_socket, data, len);
}
static void sk_proxy_write_eof (Socket *s)
{
ProxySocket *ps = container_of(s, ProxySocket, sock);
if (ps->pn) {
ps->pending_eof = true;
return;
}
sk_write_eof(ps->sub_socket);
}
static void sk_proxy_set_frozen (Socket *s, bool is_frozen)
{
ProxySocket *ps = container_of(s, ProxySocket, sock);
if (ps->pn) {
ps->freeze = is_frozen;
return;
}
/* handle any remaining buffered recv data first */
if (bufchain_size(&ps->pending_input_data) > 0) {
ps->freeze = is_frozen;
/* loop while we still have buffered data, and while we are
* unfrozen. the plug_receive call in the loop could result
* in a call back into this function refreezing the socket,
* so we have to check each time.
*/
while (!ps->freeze && bufchain_size(&ps->pending_input_data) > 0) {
char databuf[512];
ptrlen data = bufchain_prefix(&ps->pending_input_data);
if (data.len > lenof(databuf))
data.len = lenof(databuf);
memcpy(databuf, data.ptr, data.len);
bufchain_consume(&ps->pending_input_data, data.len);
plug_receive(ps->plug, 0, databuf, data.len);
}
/* if we're still frozen, we'll have to wait for another
* call from the backend to finish unbuffering the data.
*/
if (ps->freeze) return;
}
sk_set_frozen(ps->sub_socket, is_frozen);
}
static const char * sk_proxy_socket_error (Socket *s)
{
ProxySocket *ps = container_of(s, ProxySocket, sock);
if (ps->error != NULL || ps->sub_socket == NULL) {
return ps->error;
}
return sk_socket_error(ps->sub_socket);
}
/* basic proxy plug functions */
static void plug_proxy_log(Plug *plug, PlugLogType type, SockAddr *addr,
int port, const char *error_msg, int error_code)
{
ProxySocket *ps = container_of(plug, ProxySocket, plugimpl);
plug_log(ps->plug, type, addr, port, error_msg, error_code);
}
static void plug_proxy_closing(Plug *p, PlugCloseType type,
const char *error_msg)
{
ProxySocket *ps = container_of(p, ProxySocket, plugimpl);
proxy_negotiator_cleanup(ps);
plug_closing(ps->plug, type, error_msg);
}
static void proxy_negotiate(ProxySocket *ps)
{
assert(ps->pn);
proxy_negotiator_process_queue(ps->pn);
if (ps->pn->error) {
char *err = dupprintf("Proxy error: %s", ps->pn->error);
sfree(ps->pn->error);
proxy_negotiator_cleanup(ps);
plug_closing_error(ps->plug, err);
sfree(err);
return;
} else if (ps->pn->aborted) {
proxy_negotiator_cleanup(ps);
plug_closing_user_abort(ps->plug);
return;
}
while (bufchain_size(&ps->output_from_negotiator)) {
ptrlen data = bufchain_prefix(&ps->output_from_negotiator);
sk_write(ps->sub_socket, data.ptr, data.len);
bufchain_consume(&ps->output_from_negotiator, data.len);
}
if (ps->pn->done)
proxy_activate(ps);
}
static void plug_proxy_receive(
Plug *p, int urgent, const char *data, size_t len)
{
ProxySocket *ps = container_of(p, ProxySocket, plugimpl);
if (ps->pn) {
/* we will lose the urgentness of this data, but since most,
* if not all, of this data will be consumed by the negotiation
* process, hopefully it won't affect the protocol above us
*/
bufchain_add(&ps->pending_input_data, data, len);
proxy_negotiate(ps);
} else {
plug_receive(ps->plug, urgent, data, len);
}
}
static void plug_proxy_sent (Plug *p, size_t bufsize)
{
ProxySocket *ps = container_of(p, ProxySocket, plugimpl);
if (ps->pn)
return;
plug_sent(ps->plug, bufsize);
}
static int plug_proxy_accepting(Plug *p,
accept_fn_t constructor, accept_ctx_t ctx)
{
unreachable("ProxySockets never create listening Sockets");
}
/*
* This function can accept a NULL pointer as `addr', in which case
* it will only check the host name.
*/
static bool proxy_for_destination(SockAddr *addr, const char *hostname,
int port, Conf *conf)
{
int s = 0, e = 0;
char hostip[64];
int hostip_len, hostname_len;
const char *exclude_list;
/*
* Special local connections such as Unix-domain sockets
* unconditionally cannot be proxied, even in proxy-localhost
* mode. There just isn't any way to ask any known proxy type for
* them.
*/
if (addr && sk_address_is_special_local(addr))
return false; /* do not proxy */
/*
* Check the host name and IP against the hard-coded
* representations of `localhost'.
*/
if (!conf_get_bool(conf, CONF_even_proxy_localhost) &&
(sk_hostname_is_local(hostname) ||
(addr && sk_address_is_local(addr))))
return false; /* do not proxy */
/* we want a string representation of the IP address for comparisons */
if (addr) {
sk_getaddr(addr, hostip, 64);
hostip_len = strlen(hostip);
} else
hostip_len = 0; /* placate gcc; shouldn't be required */
hostname_len = strlen(hostname);
exclude_list = conf_get_str(conf, CONF_proxy_exclude_list);
/* now parse the exclude list, and see if either our IP
* or hostname matches anything in it.
*/
while (exclude_list[s]) {
while (exclude_list[s] &&
(isspace((unsigned char)exclude_list[s]) ||
exclude_list[s] == ',')) s++;
if (!exclude_list[s]) break;
e = s;
while (exclude_list[e] &&
(isalnum((unsigned char)exclude_list[e]) ||
exclude_list[e] == '-' ||
exclude_list[e] == '.' ||
exclude_list[e] == '*')) e++;
if (exclude_list[s] == '*') {
/* wildcard at beginning of entry */
if ((addr && strnicmp(hostip + hostip_len - (e - s - 1),
exclude_list + s + 1, e - s - 1) == 0) ||
strnicmp(hostname + hostname_len - (e - s - 1),
exclude_list + s + 1, e - s - 1) == 0) {
/* IP/hostname range excluded. do not use proxy. */
return false;
}
} else if (exclude_list[e-1] == '*') {
/* wildcard at end of entry */
if ((addr && strnicmp(hostip, exclude_list + s, e - s - 1) == 0) ||
strnicmp(hostname, exclude_list + s, e - s - 1) == 0) {
/* IP/hostname range excluded. do not use proxy. */
return false;
}
} else {
/* no wildcard at either end, so let's try an absolute
* match (ie. a specific IP)
*/
if (addr && strnicmp(hostip, exclude_list + s, e - s) == 0)
return false; /* IP/hostname excluded. do not use proxy. */
if (strnicmp(hostname, exclude_list + s, e - s) == 0)
return false; /* IP/hostname excluded. do not use proxy. */
}
s = e;
/* Make sure we really have reached the next comma or end-of-string */
while (exclude_list[s] &&
!isspace((unsigned char)exclude_list[s]) &&
exclude_list[s] != ',') s++;
}
/* no matches in the exclude list, so use the proxy */
return true;
}
static char *dns_log_msg(const char *host, int addressfamily,
const char *reason)
{
return dupprintf("Looking up host \"%s\"%s for %s", host,
(addressfamily == ADDRTYPE_IPV4 ? " (IPv4)" :
addressfamily == ADDRTYPE_IPV6 ? " (IPv6)" :
""), reason);
}
SockAddr *name_lookup(const char *host, int port, char **canonicalname,
Conf *conf, int addressfamily, LogContext *logctx,
const char *reason)
{
if (conf_get_int(conf, CONF_proxy_type) != PROXY_NONE &&
do_proxy_dns(conf) &&
proxy_for_destination(NULL, host, port, conf)) {
if (logctx)
logeventf(logctx, "Leaving host lookup to proxy of \"%s\""
" (for %s)", host, reason);
*canonicalname = dupstr(host);
return sk_nonamelookup(host);
} else {
if (logctx)
logevent_and_free(
logctx, dns_log_msg(host, addressfamily, reason));
return sk_namelookup(host, canonicalname, addressfamily);
}
}
static const SocketVtable ProxySocket_sockvt = {
.plug = sk_proxy_plug,
.close = sk_proxy_close,
.write = sk_proxy_write,
.write_oob = sk_proxy_write_oob,
.write_eof = sk_proxy_write_eof,
.set_frozen = sk_proxy_set_frozen,
.socket_error = sk_proxy_socket_error,
.peer_info = NULL,
};
static const PlugVtable ProxySocket_plugvt = {
.log = plug_proxy_log,
.closing = plug_proxy_closing,
.receive = plug_proxy_receive,
.sent = plug_proxy_sent,
.accepting = plug_proxy_accepting
};
static char *proxy_description(Interactor *itr)
{
ProxySocket *ps = container_of(itr, ProxySocket, interactor);
assert(ps->pn);
return dupprintf("%s connection to %s port %d", ps->pn->vt->type,
conf_get_str(ps->conf, CONF_proxy_host),
conf_get_int(ps->conf, CONF_proxy_port));
}
static LogPolicy *proxy_logpolicy(Interactor *itr)
{
ProxySocket *ps = container_of(itr, ProxySocket, interactor);
return ps->clientlp;
}
static Seat *proxy_get_seat(Interactor *itr)
{
ProxySocket *ps = container_of(itr, ProxySocket, interactor);
return ps->clientseat;
}
static void proxy_set_seat(Interactor *itr, Seat *seat)
{
ProxySocket *ps = container_of(itr, ProxySocket, interactor);
ps->clientseat = seat;
}
static const InteractorVtable ProxySocket_interactorvt = {
.description = proxy_description,
.logpolicy = proxy_logpolicy,
.get_seat = proxy_get_seat,
.set_seat = proxy_set_seat,
};
static void proxy_prompts_callback(void *ctx)
{
proxy_negotiate((ProxySocket *)ctx);
}
prompts_t *proxy_new_prompts(ProxySocket *ps)
{
prompts_t *prs = new_prompts();
prs->callback = proxy_prompts_callback;
prs->callback_ctx = ps;
return prs;
}
void proxy_spr_abort(ProxyNegotiator *pn, SeatPromptResult spr)
{
if (spr.kind == SPRK_SW_ABORT) {
pn->error = spr_get_error_message(spr);
} else {
assert(spr.kind == SPRK_USER_ABORT);
pn->aborted = true;
}
}
Socket *new_connection(SockAddr *addr, const char *hostname,
int port, bool privport,
bool oobinline, bool nodelay, bool keepalive,
Plug *plug, Conf *conf, Interactor *itr)
{
int type = conf_get_int(conf, CONF_proxy_type);
if (type != PROXY_NONE &&
proxy_for_destination(addr, hostname, port, conf))
{
ProxySocket *ps;
SockAddr *proxy_addr;
char *proxy_canonical_name;
Socket *sret;
if (type == PROXY_SSH &&
(sret = sshproxy_new_connection(addr, hostname, port, privport,
oobinline, nodelay, keepalive,
plug, conf, itr)) != NULL)
return sret;
if ((sret = platform_new_connection(addr, hostname, port, privport,
oobinline, nodelay, keepalive,
plug, conf, itr)) != NULL)
return sret;
ps = snew(ProxySocket);
ps->sock.vt = &ProxySocket_sockvt;
ps->plugimpl.vt = &ProxySocket_plugvt;
ps->interactor.vt = &ProxySocket_interactorvt;
ps->conf = conf_copy(conf);
ps->plug = plug;
ps->remote_addr = addr; /* will need to be freed on close */
ps->remote_port = port;
ps->error = NULL;
ps->pending_eof = false;
ps->freeze = false;
bufchain_init(&ps->pending_input_data);
bufchain_init(&ps->pending_output_data);
bufchain_init(&ps->pending_oob_output_data);
bufchain_init(&ps->output_from_negotiator);
ps->sub_socket = NULL;
/*
* If we've been given an Interactor by the caller, set ourselves
* up to work with it.
*/
if (itr) {
ps->clientitr = itr;
interactor_set_child(ps->clientitr, &ps->interactor);
ps->clientlp = interactor_logpolicy(ps->clientitr);
ps->clientseat = interactor_borrow_seat(ps->clientitr);
}
const ProxyNegotiatorVT *vt;
switch (type) {
case PROXY_HTTP:
vt = &http_proxy_negotiator_vt;
break;
case PROXY_SOCKS4:
vt = &socks4_proxy_negotiator_vt;
break;
case PROXY_SOCKS5:
vt = &socks5_proxy_negotiator_vt;
break;
case PROXY_TELNET:
vt = &telnet_proxy_negotiator_vt;
break;
default:
ps->error = "Proxy error: Unknown proxy method";
return &ps->sock;
}
ps->pn = proxy_negotiator_new(vt);
ps->pn->ps = ps;
ps->pn->done = false;
ps->pn->error = NULL;
ps->pn->aborted = false;
ps->pn->input = &ps->pending_input_data;
/* Provide an Interactor to the negotiator if and only if we
* are usefully able to ask interactive questions of the user */
ps->pn->itr = ps->clientseat ? &ps->interactor : NULL;
bufchain_sink_init(ps->pn->output, &ps->output_from_negotiator);
{
char *logmsg = dupprintf("Will use %s proxy at %s:%d to connect"
" to %s:%d", vt->type,
conf_get_str(conf, CONF_proxy_host),
conf_get_int(conf, CONF_proxy_port),
hostname, port);
plug_log(plug, PLUGLOG_PROXY_MSG, NULL, 0, logmsg, 0);
sfree(logmsg);
}
{
char *logmsg = dns_log_msg(conf_get_str(conf, CONF_proxy_host),
conf_get_int(conf, CONF_addressfamily),
"proxy");
plug_log(plug, PLUGLOG_PROXY_MSG, NULL, 0, logmsg, 0);
sfree(logmsg);
}
/* look-up proxy */
proxy_addr = sk_namelookup(conf_get_str(conf, CONF_proxy_host),
&proxy_canonical_name,
conf_get_int(conf, CONF_addressfamily));
if (sk_addr_error(proxy_addr) != NULL) {
ps->error = "Proxy error: Unable to resolve proxy host name";
sk_addr_free(proxy_addr);
return &ps->sock;
}
sfree(proxy_canonical_name);
{
char addrbuf[256], *logmsg;
sk_getaddr(proxy_addr, addrbuf, lenof(addrbuf));
logmsg = dupprintf("Connecting to %s proxy at %s port %d",
vt->type, addrbuf,
conf_get_int(conf, CONF_proxy_port));
plug_log(plug, PLUGLOG_PROXY_MSG, NULL, 0, logmsg, 0);
sfree(logmsg);
}
/* create the actual socket we will be using,
* connected to our proxy server and port.
*/
ps->sub_socket = sk_new(proxy_addr,
conf_get_int(conf, CONF_proxy_port),
privport, oobinline,
nodelay, keepalive, &ps->plugimpl);
if (sk_socket_error(ps->sub_socket) != NULL)
return &ps->sock;
/* start the proxy negotiation process... */
sk_set_frozen(ps->sub_socket, false);
proxy_negotiate(ps);
return &ps->sock;
}
/* no proxy, so just return the direct socket */
return sk_new(addr, port, privport, oobinline, nodelay, keepalive, plug);
}
Socket *new_listener(const char *srcaddr, int port, Plug *plug,
bool local_host_only, Conf *conf, int addressfamily)
{
/* TODO: SOCKS (and potentially others) support inbound
* TODO: connections via the proxy. support them.
*/
return sk_newlistener(srcaddr, port, plug, local_host_only, addressfamily);
}