diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ccd8570..4b60c9d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,7 +34,12 @@ add_subdirectory(crypto) add_library(network STATIC be_misc.c nullplug.c errsock.c logging.c x11disp.c - proxy/proxy.c proxy/interactor.c) + proxy/proxy.c + proxy/http.c + proxy/socks4.c + proxy/socks5.c + proxy/telnet.c + proxy/interactor.c) add_library(keygen STATIC import.c) diff --git a/proxy/cproxy.c b/proxy/cproxy.c index 14c67357..fab5f63c 100644 --- a/proxy/cproxy.c +++ b/proxy/cproxy.c @@ -13,163 +13,14 @@ #include "ssh.h" /* For MD5 support */ #include "network.h" #include "proxy.h" -#include "socks.h" #include "marshal.h" -static void hmacmd5_chap(const unsigned char *challenge, int challen, - const char *passwd, unsigned char *response) +const bool socks5_chap_available = true; + +strbuf *chap_response(ptrlen challenge, ptrlen password) { - mac_simple(&ssh_hmac_md5, ptrlen_from_asciz(passwd), - make_ptrlen(challenge, challen), response); -} - -void proxy_socks5_offerencryptedauth(BinarySink *bs) -{ - put_byte(bs, SOCKS5_AUTH_CHAP); -} - -int proxy_socks5_handlechap (ProxySocket *ps) -{ - - /* CHAP authentication reply format: - * version number (1 bytes) = 1 - * number of commands (1 byte) - * - * For each command: - * command identifier (1 byte) - * data length (1 byte) - */ - unsigned char data[260]; - unsigned char outbuf[20]; - - while(ps->chap_num_attributes == 0 || - ps->chap_num_attributes_processed < ps->chap_num_attributes) { - if (ps->chap_num_attributes == 0 || - ps->chap_current_attribute == -1) { - /* CHAP normally reads in two bytes, either at the - * beginning or for each attribute/value pair. But if - * we're waiting for the value's data, we might not want - * to read 2 bytes. - */ - - if (bufchain_size(&ps->pending_input_data) < 2) - return 1; /* not got anything yet */ - - /* get the response */ - bufchain_fetch(&ps->pending_input_data, data, 2); - bufchain_consume(&ps->pending_input_data, 2); - } - - if (ps->chap_num_attributes == 0) { - /* If there are no attributes, this is our first msg - * with the server, where we negotiate version and - * number of attributes - */ - if (data[0] != SOCKS5_AUTH_CHAP_VERSION) { - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy wants " - "a different CHAP version"); - return 1; - } - if (data[1] == 0x00) { - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy won't " - "negotiate CHAP with us"); - return 1; - } - ps->chap_num_attributes = data[1]; - } else { - if (ps->chap_current_attribute == -1) { - /* We have to read in each attribute/value pair - - * those we don't understand can be ignored, but - * there are a few we'll need to handle. - */ - ps->chap_current_attribute = data[0]; - ps->chap_current_datalen = data[1]; - } - if (bufchain_size(&ps->pending_input_data) < - ps->chap_current_datalen) - return 1; /* not got everything yet */ - - /* get the response */ - bufchain_fetch(&ps->pending_input_data, data, - ps->chap_current_datalen); - - bufchain_consume(&ps->pending_input_data, - ps->chap_current_datalen); - - switch (ps->chap_current_attribute) { - case SOCKS5_AUTH_CHAP_ATTR_STATUS: - /* Successful authentication */ - if (data[0] == 0x00) - ps->state = 2; - else { - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy " - "refused CHAP authentication"); - return 1; - } - break; - case SOCKS5_AUTH_CHAP_ATTR_CHALLENGE: - outbuf[0] = SOCKS5_AUTH_CHAP_VERSION; - outbuf[1] = 0x01; /* One attribute */ - outbuf[2] = SOCKS5_AUTH_CHAP_ATTR_RESPONSE; - outbuf[3] = 0x10; /* Length */ - hmacmd5_chap(data, ps->chap_current_datalen, - conf_get_str(ps->conf, CONF_proxy_password), - &outbuf[4]); - sk_write(ps->sub_socket, outbuf, 20); - break; - case SOCKS5_AUTH_CHAP_ATTR_ALGLIST: - /* Chose a protocol */ - if (data[0] != SOCKS5_AUTH_CHAP_ALG_HMACMD5) { - plug_closing_error(ps->plug, "Proxy error: Server chose " - "CHAP of other than HMAC-MD5 but we " - "didn't offer it!"); - return 1; - } - break; - } - ps->chap_current_attribute = -1; - ps->chap_num_attributes_processed++; - } - if (ps->state == 8 && - ps->chap_num_attributes_processed >= ps->chap_num_attributes) { - ps->chap_num_attributes = 0; - ps->chap_num_attributes_processed = 0; - ps->chap_current_datalen = 0; - } - } - return 0; -} - -int proxy_socks5_selectchap(ProxySocket *ps) -{ - char *username = conf_get_str(ps->conf, CONF_proxy_username); - char *password = conf_get_str(ps->conf, CONF_proxy_password); - if (username[0] || password[0]) { - char chapbuf[514]; - int ulen; - chapbuf[0] = SOCKS5_AUTH_CHAP_VERSION; - chapbuf[1] = '\x02'; /* Number of attributes sent */ - chapbuf[2] = SOCKS5_AUTH_CHAP_ATTR_ALGLIST; - chapbuf[3] = '\x01'; /* Only one CHAP algorithm */ - chapbuf[4] = SOCKS5_AUTH_CHAP_ALG_HMACMD5; - chapbuf[5] = SOCKS5_AUTH_CHAP_ATTR_USERNAME; - - ulen = strlen(username); - if (ulen > 255) ulen = 255; - if (ulen < 1) ulen = 1; - - chapbuf[6] = ulen; - memcpy(chapbuf+7, username, ulen); - - sk_write(ps->sub_socket, chapbuf, ulen + 7); - ps->chap_num_attributes = 0; - ps->chap_num_attributes_processed = 0; - ps->chap_current_attribute = -1; - ps->chap_current_datalen = 0; - - ps->state = 8; - } else - plug_closing_error(ps->plug, "Proxy error: Server chose " - "CHAP authentication but we didn't offer it!"); - return 1; + strbuf *sb = strbuf_new_nm(); + const ssh2_macalg *alg = &ssh_hmac_md5; + mac_simple(alg, password, challenge, strbuf_append(sb, alg->len)); + return sb; } diff --git a/proxy/http.c b/proxy/http.c new file mode 100644 index 00000000..d67f1be9 --- /dev/null +++ b/proxy/http.c @@ -0,0 +1,158 @@ +/* + * HTTP CONNECT proxy negotiation. + */ + +#include "putty.h" +#include "network.h" +#include "proxy.h" +#include "sshcr.h" + +static bool read_line(bufchain *input, strbuf *output, bool is_header) +{ + char c; + + while (bufchain_try_fetch(input, &c, 1)) { + if (is_header && output->len > 0 && + output->s[output->len - 1] == '\n') { + /* + * A newline terminates the header, provided we're sure it + * is _not_ followed by a space or a tab. + */ + if (c != ' ' && c != '\t') + goto done; /* we have a complete header line */ + } else { + put_byte(output, c); + bufchain_consume(input, 1); + + if (!is_header && output->len > 0 && + output->s[output->len - 1] == '\n') { + /* If we're looking for just a line, not an HTTP + * header, then any newline terminates it. */ + goto done; + } + } + } + + return false; + + done: + strbuf_chomp(output, '\n'); + strbuf_chomp(output, '\r'); + return true; +} + +typedef struct HttpProxyNegotiator { + int crLine; + strbuf *line; + ProxyNegotiator pn; +} HttpProxyNegotiator; + +static ProxyNegotiator *proxy_http_new(const ProxyNegotiatorVT *vt) +{ + HttpProxyNegotiator *s = snew(HttpProxyNegotiator); + s->pn.vt = vt; + s->crLine = 0; + s->line = strbuf_new(); + return &s->pn; +} + +static void proxy_http_free(ProxyNegotiator *pn) +{ + HttpProxyNegotiator *s = container_of(pn, HttpProxyNegotiator, pn); + strbuf_free(s->line); + sfree(s); +} + +static void proxy_http_process_queue(ProxyNegotiator *pn) +{ + HttpProxyNegotiator *s = container_of(pn, HttpProxyNegotiator, pn); + + crBegin(s->crLine); + + /* + * Standard prefix for the HTTP CONNECT request. + */ + { + char dest[512]; + sk_getaddr(pn->ps->remote_addr, dest, lenof(dest)); + put_fmt(pn->output, + "CONNECT %s:%d HTTP/1.1\r\n" + "Host: %s:%d\r\n", + dest, pn->ps->remote_port, dest, pn->ps->remote_port); + } + + /* + * Optionally send an HTTP Basic auth header with the username and + * password. + */ + { + const char *username = conf_get_str(pn->ps->conf, CONF_proxy_username); + const char *password = conf_get_str(pn->ps->conf, CONF_proxy_password); + if (username[0] || password[0]) { + put_datalit(pn->output, "Proxy-Authorization: Basic "); + + char *base64_input = dupcat(username, ":", password); + char base64_output[4]; + for (size_t i = 0, e = strlen(base64_input); i < e; i += 3) { + base64_encode_atom((const unsigned char *)base64_input + i, + e-i > 3 ? 3 : e-i, base64_output); + put_data(pn->output, base64_output, 4); + } + burnstr(base64_input); + smemclr(base64_output, sizeof(base64_output)); + put_datalit(pn->output, "\r\n"); + } + } + + /* + * Blank line to terminate the HTTP request. + */ + put_datalit(pn->output, "\r\n"); + crReturnV; + + /* + * Read and parse the HTTP status line, and check if it's a 2xx + * for success. + */ + strbuf_clear(s->line); + crMaybeWaitUntilV(read_line(pn->input, s->line, false)); + { + int maj_ver, min_ver, status_pos = -1; + sscanf(s->line->s, "HTTP/%d.%d %n", &maj_ver, &min_ver, &status_pos); + + /* If status_pos is still -1 then the sscanf didn't get right + * to the end of the string */ + if (status_pos == -1) { + pn->error = dupstr("HTTP response was absent or malformed"); + crStopV; + } + + if (s->line->s[status_pos] != '2') { + pn->error = dupprintf("HTTP response %s", s->line->s + status_pos); + crStopV; + } + } + + /* + * Read and skip the rest of the HTTP response headers, terminated + * by a blank line. + */ + do { + strbuf_clear(s->line); + crMaybeWaitUntilV(read_line(pn->input, s->line, true)); + } while (s->line->len > 0); + + /* + * Success! Hand over to the main connection. + */ + pn->done = true; + + crFinishV; +} + +const struct ProxyNegotiatorVT http_proxy_negotiator_vt = { + .new = proxy_http_new, + .free = proxy_http_free, + .process_queue = proxy_http_process_queue, + .type = "HTTP", +}; diff --git a/proxy/nocproxy.c b/proxy/nocproxy.c index ed9c3ffd..85beec36 100644 --- a/proxy/nocproxy.c +++ b/proxy/nocproxy.c @@ -12,21 +12,9 @@ #include "network.h" #include "proxy.h" -void proxy_socks5_offerencryptedauth(BinarySink *bs) -{ - /* For telnet, don't add any new encrypted authentication routines */ -} +const bool socks5_chap_available = false; -int proxy_socks5_handlechap(ProxySocket *p) +strbuf *chap_response(ptrlen challenge, ptrlen password) { - plug_closing_error(p->plug, "Proxy error: Trying to handle a " - "SOCKS5 CHAP request in telnet-only build"); - return 1; -} - -int proxy_socks5_selectchap(ProxySocket *p) -{ - plug_closing_error(p->plug, "Proxy error: Trying to handle a " - "SOCKS5 CHAP request in telnet-only build"); - return 1; + unreachable("CHAP is not built into this binary"); } diff --git a/proxy/proxy.c b/proxy/proxy.c index b233bba5..249b39a4 100644 --- a/proxy/proxy.c +++ b/proxy/proxy.c @@ -12,7 +12,6 @@ #include "putty.h" #include "network.h" #include "proxy.h" -#include "socks.h" #define do_proxy_dns(conf) \ (conf_get_int(conf, CONF_proxy_dns) == FORCE_ON || \ @@ -27,7 +26,9 @@ void proxy_activate(ProxySocket *ps) { size_t output_before, output_after; - ps->state = PROXY_STATE_ACTIVE; + assert(ps->pn); + proxy_negotiator_free(ps->pn); + ps->pn = NULL; plug_log(ps->plug, PLUGLOG_CONNECT_SUCCESS, NULL, 0, NULL, 0); @@ -88,6 +89,9 @@ static void sk_proxy_close (Socket *s) sk_close(ps->sub_socket); sk_addr_free(ps->remote_addr); + if (ps->pn) + proxy_negotiator_free(ps->pn); + bufchain_clear(&ps->output_from_negotiator); sfree(ps); } @@ -95,7 +99,7 @@ static size_t sk_proxy_write (Socket *s, const void *data, size_t len) { ProxySocket *ps = container_of(s, ProxySocket, sock); - if (ps->state != PROXY_STATE_ACTIVE) { + if (ps->pn) { bufchain_add(&ps->pending_output_data, data, len); return bufchain_size(&ps->pending_output_data); } @@ -106,7 +110,7 @@ static size_t sk_proxy_write_oob (Socket *s, const void *data, size_t len) { ProxySocket *ps = container_of(s, ProxySocket, sock); - if (ps->state != PROXY_STATE_ACTIVE) { + 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); @@ -119,7 +123,7 @@ static void sk_proxy_write_eof (Socket *s) { ProxySocket *ps = container_of(s, ProxySocket, sock); - if (ps->state != PROXY_STATE_ACTIVE) { + if (ps->pn) { ps->pending_eof = true; return; } @@ -130,7 +134,7 @@ static void sk_proxy_set_frozen (Socket *s, bool is_frozen) { ProxySocket *ps = container_of(s, ProxySocket, sock); - if (ps->state != PROXY_STATE_ACTIVE) { + if (ps->pn) { ps->freeze = is_frozen; return; } @@ -190,21 +194,40 @@ static void plug_proxy_closing(Plug *p, PlugCloseType type, 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->done) { + proxy_activate(ps); + } else if (ps->pn->error) { + char *err = dupprintf("Proxy error: %s", ps->pn->error); + sfree(ps->pn->error); + proxy_negotiator_free(ps->pn); + ps->pn = NULL; + plug_closing_error(ps->plug, err); + sfree(err); + } else { + 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); + } + } +} + static void plug_proxy_receive( Plug *p, int urgent, const char *data, size_t len) { ProxySocket *ps = container_of(p, ProxySocket, plugimpl); - if (ps->state != PROXY_STATE_ACTIVE) { + 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); - ps->receive_urgent = (urgent != 0); - ps->receive_data = data; - ps->receive_len = len; - ps->negotiate(ps, PROXY_CHANGE_RECEIVE); + proxy_negotiate(ps); } else { plug_receive(ps->plug, urgent, data, len); } @@ -214,9 +237,8 @@ static void plug_proxy_sent (Plug *p, size_t bufsize) { ProxySocket *ps = container_of(p, ProxySocket, plugimpl); - if (ps->state != PROXY_STATE_ACTIVE) + if (ps->pn) return; - plug_sent(ps->plug, bufsize); } @@ -391,7 +413,6 @@ Socket *new_connection(SockAddr *addr, const char *hostname, ProxySocket *ps; SockAddr *proxy_addr; char *proxy_canonical_name; - const char *proxy_type; Socket *sret; if (type == PROXY_SSH && @@ -420,31 +441,38 @@ Socket *new_connection(SockAddr *addr, const char *hostname, 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; - ps->state = PROXY_STATE_NEW; - ps->negotiate = NULL; - if (type == PROXY_HTTP) { - ps->negotiate = proxy_http_negotiate; - proxy_type = "HTTP"; - } else if (type == PROXY_SOCKS4) { - ps->negotiate = proxy_socks4_negotiate; - proxy_type = "SOCKS 4"; - } else if (type == PROXY_SOCKS5) { - ps->negotiate = proxy_socks5_negotiate; - proxy_type = "SOCKS 5"; - } else if (type == PROXY_TELNET) { - ps->negotiate = proxy_telnet_negotiate; - proxy_type = "Telnet"; - } else { + 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->input = &ps->pending_input_data; + 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", proxy_type, + " to %s:%d", vt->type, conf_get_str(conf, CONF_proxy_host), conf_get_int(conf, CONF_proxy_port), hostname, port); @@ -475,7 +503,7 @@ Socket *new_connection(SockAddr *addr, const char *hostname, char addrbuf[256], *logmsg; sk_getaddr(proxy_addr, addrbuf, lenof(addrbuf)); logmsg = dupprintf("Connecting to %s proxy at %s port %d", - proxy_type, addrbuf, + vt->type, addrbuf, conf_get_int(conf, CONF_proxy_port)); plug_log(plug, PLUGLOG_PROXY_MSG, NULL, 0, logmsg, 0); sfree(logmsg); @@ -493,7 +521,7 @@ Socket *new_connection(SockAddr *addr, const char *hostname, /* start the proxy negotiation process... */ sk_set_frozen(ps->sub_socket, false); - ps->negotiate(ps, PROXY_CHANGE_NEW); + proxy_negotiate(ps); return &ps->sock; } @@ -511,865 +539,3 @@ Socket *new_listener(const char *srcaddr, int port, Plug *plug, return sk_newlistener(srcaddr, port, plug, local_host_only, addressfamily); } - -/* ---------------------------------------------------------------------- - * HTTP CONNECT proxy type. - */ - -static bool get_line_end(char *data, size_t len, size_t *out) -{ - size_t off = 0; - - while (off < len) - { - if (data[off] == '\n') { - /* we have a newline */ - off++; - - /* is that the only thing on this line? */ - if (off <= 2) { - *out = off; - return true; - } - - /* if not, then there is the possibility that this header - * continues onto the next line, if it starts with a space - * or a tab. - */ - - if (off + 1 < len && data[off+1] != ' ' && data[off+1] != '\t') { - *out = off; - return true; - } - - /* the line does continue, so we have to keep going - * until we see an the header's "real" end of line. - */ - off++; - } - - off++; - } - - return false; -} - -int proxy_http_negotiate (ProxySocket *ps, int change) -{ - if (ps->state == PROXY_STATE_NEW) { - /* we are just beginning the proxy negotiate process, - * so we'll send off the initial bits of the request. - * for this proxy method, it's just a simple HTTP - * request - */ - char *buf, dest[512]; - char *username, *password; - - sk_getaddr(ps->remote_addr, dest, lenof(dest)); - - buf = dupprintf("CONNECT %s:%i HTTP/1.1\r\nHost: %s:%i\r\n", - dest, ps->remote_port, dest, ps->remote_port); - sk_write(ps->sub_socket, buf, strlen(buf)); - sfree(buf); - - username = conf_get_str(ps->conf, CONF_proxy_username); - password = conf_get_str(ps->conf, CONF_proxy_password); - if (username[0] || password[0]) { - char *buf, *buf2; - int i, j, len; - buf = dupprintf("%s:%s", username, password); - len = strlen(buf); - buf2 = snewn(len * 4 / 3 + 100, char); - sprintf(buf2, "Proxy-Authorization: Basic "); - for (i = 0, j = strlen(buf2); i < len; i += 3, j += 4) - base64_encode_atom((unsigned char *)(buf+i), - (len-i > 3 ? 3 : len-i), buf2+j); - strcpy(buf2+j, "\r\n"); - sk_write(ps->sub_socket, buf2, strlen(buf2)); - sfree(buf); - sfree(buf2); - } - - sk_write(ps->sub_socket, "\r\n", 2); - - ps->state = 1; - return 0; - } - - if (change == PROXY_CHANGE_RECEIVE) { - /* we have received data from the underlying socket, which - * we'll need to parse, process, and respond to appropriately. - */ - - char *data, *datap; - size_t len, eol; - - if (ps->state == 1) { - - int min_ver, maj_ver, status; - - /* get the status line */ - len = bufchain_size(&ps->pending_input_data); - assert(len > 0); /* or we wouldn't be here */ - data = snewn(len+1, char); - bufchain_fetch(&ps->pending_input_data, data, len); - /* - * We must NUL-terminate this data, because Windows - * sscanf appears to require a NUL at the end of the - * string because it strlens it _first_. Sigh. - */ - data[len] = '\0'; - - if (!get_line_end(data, len, &eol)) { - sfree(data); - return 1; - } - - status = -1; - /* We can't rely on whether the %n incremented the sscanf return */ - if (sscanf((char *)data, "HTTP/%i.%i %n", - &maj_ver, &min_ver, &status) < 2 || status == -1) { - plug_closing_error(ps->plug, "Proxy error: " - "HTTP response was absent"); - sfree(data); - return 1; - } - - /* remove the status line from the input buffer. */ - bufchain_consume(&ps->pending_input_data, eol); - if (data[status] != '2') { - /* error */ - char *buf; - data[eol] = '\0'; - while (eol > status && - (data[eol-1] == '\r' || data[eol-1] == '\n')) - data[--eol] = '\0'; - buf = dupprintf("Proxy error: %s", data+status); - plug_closing_error(ps->plug, buf); - sfree(buf); - sfree(data); - return 1; - } - - sfree(data); - - ps->state = 2; - } - - if (ps->state == 2) { - - /* get headers. we're done when we get a - * header of length 2, (ie. just "\r\n") - */ - - len = bufchain_size(&ps->pending_input_data); - assert(len > 0); /* or we wouldn't be here */ - data = snewn(len, char); - datap = data; - bufchain_fetch(&ps->pending_input_data, data, len); - - if (!get_line_end(datap, len, &eol)) { - sfree(data); - return 1; - } - while (eol > 2) { - bufchain_consume(&ps->pending_input_data, eol); - datap += eol; - len -= eol; - if (!get_line_end(datap, len, &eol)) - eol = 0; /* terminate the loop */ - } - - if (eol == 2) { - /* we're done */ - bufchain_consume(&ps->pending_input_data, 2); - proxy_activate(ps); - /* proxy activate will have dealt with - * whatever is left of the buffer */ - sfree(data); - return 1; - } - - sfree(data); - return 1; - } - } - - plug_closing_error(ps->plug, "Proxy error: unexpected proxy error"); - return 1; -} - -/* ---------------------------------------------------------------------- - * SOCKS proxy type. - */ - -/* SOCKS version 4 */ -int proxy_socks4_negotiate (ProxySocket *ps, int change) -{ - if (ps->state == PROXY_CHANGE_NEW) { - - /* request format: - * version number (1 byte) = 4 - * command code (1 byte) - * 1 = CONNECT - * 2 = BIND - * dest. port (2 bytes) [network order] - * dest. address (4 bytes) - * user ID (variable length, null terminated string) - */ - - strbuf *command = strbuf_new(); - char hostname[512]; - bool write_hostname = false; - - put_byte(command, SOCKS4_REQUEST_VERSION); - put_byte(command, SOCKS_CMD_CONNECT); - put_uint16(command, ps->remote_port); - - switch (sk_addrtype(ps->remote_addr)) { - case ADDRTYPE_IPV4: { - char addr[4]; - sk_addrcopy(ps->remote_addr, addr); - put_data(command, addr, 4); - break; - } - case ADDRTYPE_NAME: - sk_getaddr(ps->remote_addr, hostname, lenof(hostname)); - put_uint32(command, SOCKS4A_NAME_FOLLOWS_BASE); - write_hostname = true; - break; - case ADDRTYPE_IPV6: - ps->error = "Proxy error: SOCKS version 4 does not support IPv6"; - strbuf_free(command); - return 1; - } - - put_asciz(command, conf_get_str(ps->conf, CONF_proxy_username)); - if (write_hostname) - put_asciz(command, hostname); - sk_write(ps->sub_socket, command->s, command->len); - strbuf_free(command); - - ps->state = 1; - return 0; - } - - if (change == PROXY_CHANGE_RECEIVE) { - /* we have received data from the underlying socket, which - * we'll need to parse, process, and respond to appropriately. - */ - - if (ps->state == 1) { - /* response format: - * version number (1 byte) = 0 - * reply code (1 byte) - * 90 = request granted - * 91 = request rejected or failed - * 92 = request rejected due to lack of IDENTD on client - * 93 = request rejected due to difference in user ID - * (what we sent vs. what IDENTD said) - * dest. port (2 bytes) - * dest. address (4 bytes) - */ - - char data[8]; - - if (bufchain_size(&ps->pending_input_data) < 8) - return 1; /* not got anything yet */ - - /* get the response */ - bufchain_fetch(&ps->pending_input_data, data, 8); - - if (data[0] != SOCKS4_REPLY_VERSION) { - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy " - "responded with unexpected " - "reply code version"); - return 1; - } - - if (data[1] != SOCKS4_RESP_SUCCESS) { - - switch (data[1]) { - case SOCKS4_RESP_WANT_IDENTD: - plug_closing_error(ps->plug, "Proxy error: SOCKS server " - "wanted IDENTD on client"); - break; - case SOCKS4_RESP_IDENTD_MISMATCH: - plug_closing_error(ps->plug, "Proxy error: Username and " - "IDENTD on client don't agree"); - break; - case SOCKS4_RESP_FAILURE: - default: - plug_closing_error(ps->plug, "Proxy error: Error while " - "communicating with proxy"); - break; - } - - return 1; - } - bufchain_consume(&ps->pending_input_data, 8); - - /* we're done */ - proxy_activate(ps); - /* proxy activate will have dealt with - * whatever is left of the buffer */ - return 1; - } - } - - plug_closing_error(ps->plug, "Proxy error: unexpected proxy error"); - return 1; -} - -/* SOCKS version 5 */ -int proxy_socks5_negotiate (ProxySocket *ps, int change) -{ - if (ps->state == PROXY_CHANGE_NEW) { - - /* initial command: - * version number (1 byte) = 5 - * number of available authentication methods (1 byte) - * available authentication methods (1 byte * previous value) - * authentication methods: - * 0x00 = no authentication - * 0x01 = GSSAPI - * 0x02 = username/password - * 0x03 = CHAP - */ - - strbuf *command; - char *username, *password; - int method_count_offset, methods_start; - - command = strbuf_new(); - put_byte(command, SOCKS5_REQUEST_VERSION); - username = conf_get_str(ps->conf, CONF_proxy_username); - password = conf_get_str(ps->conf, CONF_proxy_password); - - method_count_offset = command->len; - put_byte(command, 0); - methods_start = command->len; - - put_byte(command, SOCKS5_AUTH_NONE); - - if (username[0] || password[0]) { - proxy_socks5_offerencryptedauth(BinarySink_UPCAST(command)); - put_byte(command, SOCKS5_AUTH_PASSWORD); - } - - command->u[method_count_offset] = command->len - methods_start; - - sk_write(ps->sub_socket, command->s, command->len); - strbuf_free(command); - - ps->state = 1; - return 0; - } - - if (change == PROXY_CHANGE_RECEIVE) { - /* we have received data from the underlying socket, which - * we'll need to parse, process, and respond to appropriately. - */ - - if (ps->state == 1) { - - /* initial response: - * version number (1 byte) = 5 - * authentication method (1 byte) - * authentication methods: - * 0x00 = no authentication - * 0x01 = GSSAPI - * 0x02 = username/password - * 0x03 = CHAP - * 0xff = no acceptable methods - */ - char data[2]; - - if (bufchain_size(&ps->pending_input_data) < 2) - return 1; /* not got anything yet */ - - /* get the response */ - bufchain_fetch(&ps->pending_input_data, data, 2); - - if (data[0] != SOCKS5_REPLY_VERSION) { - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy " - "returned unexpected version"); - return 1; - } - - if (data[1] == SOCKS5_AUTH_NONE) ps->state = 2; - else if (data[1] == SOCKS5_AUTH_GSSAPI) ps->state = 4; - else if (data[1] == SOCKS5_AUTH_PASSWORD) ps->state = 5; - else if (data[1] == SOCKS5_AUTH_CHAP) ps->state = 6; - else { - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy did not " - "accept our authentication"); - return 1; - } - bufchain_consume(&ps->pending_input_data, 2); - } - - if (ps->state == 7) { - - /* password authentication reply format: - * version number (1 bytes) = 1 - * reply code (1 byte) - * 0 = succeeded - * >0 = failed - */ - char data[2]; - - if (bufchain_size(&ps->pending_input_data) < 2) - return 1; /* not got anything yet */ - - /* get the response */ - bufchain_fetch(&ps->pending_input_data, data, 2); - - if (data[0] != SOCKS5_AUTH_PASSWORD_VERSION) { - plug_closing_error(ps->plug, "Proxy error: SOCKS password " - "subnegotiation contained wrong version " - "number"); - return 1; - } - - if (data[1] != 0) { - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy refused " - "password authentication"); - return 1; - } - - bufchain_consume(&ps->pending_input_data, 2); - ps->state = 2; /* now proceed as authenticated */ - } - - if (ps->state == 8) { - int ret; - ret = proxy_socks5_handlechap(ps); - if (ret) return ret; - } - - if (ps->state == 2) { - - /* request format: - * version number (1 byte) = 5 - * command code (1 byte) - * 1 = CONNECT - * 2 = BIND - * 3 = UDP ASSOCIATE - * reserved (1 byte) = 0x00 - * address type (1 byte) - * 1 = IPv4 - * 3 = domainname (first byte has length, no terminating null) - * 4 = IPv6 - * dest. address (variable) - * dest. port (2 bytes) [network order] - */ - - strbuf *command = strbuf_new(); - put_byte(command, SOCKS5_REQUEST_VERSION); - put_byte(command, SOCKS_CMD_CONNECT); - put_byte(command, 0x00); /* reserved byte */ - - switch (sk_addrtype(ps->remote_addr)) { - case ADDRTYPE_IPV4: - put_byte(command, SOCKS5_ADDR_IPV4); - sk_addrcopy(ps->remote_addr, strbuf_append(command, 4)); - break; - case ADDRTYPE_IPV6: - put_byte(command, SOCKS5_ADDR_IPV6); - sk_addrcopy(ps->remote_addr, strbuf_append(command, 16)); - break; - case ADDRTYPE_NAME: { - char hostname[512]; - put_byte(command, SOCKS5_ADDR_HOSTNAME); - sk_getaddr(ps->remote_addr, hostname, lenof(hostname)); - if (!put_pstring(command, hostname)) { - ps->error = "Proxy error: SOCKS 5 cannot " - "support host names longer than 255 chars"; - strbuf_free(command); - return 1; - } - break; - } - } - - put_uint16(command, ps->remote_port); - - sk_write(ps->sub_socket, command->s, command->len); - - strbuf_free(command); - - ps->state = 3; - return 1; - } - - if (ps->state == 3) { - - /* reply format: - * version number (1 bytes) = 5 - * reply code (1 byte) - * 0 = succeeded - * 1 = general SOCKS server failure - * 2 = connection not allowed by ruleset - * 3 = network unreachable - * 4 = host unreachable - * 5 = connection refused - * 6 = TTL expired - * 7 = command not supported - * 8 = address type not supported - * reserved (1 byte) = x00 - * address type (1 byte) - * 1 = IPv4 - * 3 = domainname (first byte has length, no terminating null) - * 4 = IPv6 - * server bound address (variable) - * server bound port (2 bytes) [network order] - */ - char data[5]; - int len; - - /* First 5 bytes of packet are enough to tell its length. */ - if (bufchain_size(&ps->pending_input_data) < 5) - return 1; /* not got anything yet */ - - /* get the response */ - bufchain_fetch(&ps->pending_input_data, data, 5); - - if (data[0] != SOCKS5_REPLY_VERSION) { - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy " - "returned wrong version number"); - return 1; - } - - if (data[1] != SOCKS5_RESP_SUCCESS) { - char buf[256]; - - strcpy(buf, "Proxy error: "); - - switch (data[1]) { - case SOCKS5_RESP_FAILURE: strcat(buf, "General SOCKS server failure"); break; - case SOCKS5_RESP_CONNECTION_NOT_ALLOWED_BY_RULESET: strcat(buf, "Connection not allowed by ruleset"); break; - case SOCKS5_RESP_NETWORK_UNREACHABLE: strcat(buf, "Network unreachable"); break; - case SOCKS5_RESP_HOST_UNREACHABLE: strcat(buf, "Host unreachable"); break; - case SOCKS5_RESP_CONNECTION_REFUSED: strcat(buf, "Connection refused"); break; - case SOCKS5_RESP_TTL_EXPIRED: strcat(buf, "TTL expired"); break; - case SOCKS5_RESP_COMMAND_NOT_SUPPORTED: strcat(buf, "Command not supported"); break; - case SOCKS5_RESP_ADDRTYPE_NOT_SUPPORTED: strcat(buf, "Address type not supported"); break; - default: sprintf(buf+strlen(buf), - "Unrecognised SOCKS error code %d", - data[1]); - break; - } - plug_closing_error(ps->plug, buf); - - return 1; - } - - /* - * Eat the rest of the reply packet. - */ - len = 6; /* first 4 bytes, last 2 */ - switch (data[3]) { - case SOCKS5_ADDR_IPV4: len += 4; break; /* IPv4 address */ - case SOCKS5_ADDR_IPV6: len += 16; break;/* IPv6 address */ - case SOCKS5_ADDR_HOSTNAME: len += 1+(unsigned char)data[4]; break; /* domain name */ - default: - plug_closing_error(ps->plug, "Proxy error: SOCKS proxy " - "returned unrecognised address format"); - return 1; - } - if (bufchain_size(&ps->pending_input_data) < len) - return 1; /* not got whole reply yet */ - bufchain_consume(&ps->pending_input_data, len); - - /* we're done */ - proxy_activate(ps); - return 1; - } - - if (ps->state == 4) { - /* TODO: Handle GSSAPI authentication */ - plug_closing_error(ps->plug, "Proxy error: We don't support " - "GSSAPI authentication"); - return 1; - } - - if (ps->state == 5) { - const char *username = conf_get_str(ps->conf, CONF_proxy_username); - const char *password = conf_get_str(ps->conf, CONF_proxy_password); - if (username[0] || password[0]) { - strbuf *auth = strbuf_new_nm(); - put_byte(auth, SOCKS5_AUTH_PASSWORD_VERSION); - if (!put_pstring(auth, username)) { - ps->error = "Proxy error: SOCKS 5 authentication cannot " - "support usernames longer than 255 chars"; - strbuf_free(auth); - return 1; - } - if (!put_pstring(auth, password)) { - ps->error = "Proxy error: SOCKS 5 authentication cannot " - "support passwords longer than 255 chars"; - strbuf_free(auth); - return 1; - } - sk_write(ps->sub_socket, auth->s, auth->len); - strbuf_free(auth); - ps->state = 7; - } else - plug_closing_error(ps->plug, "Proxy error: Server chose " - "username/password authentication " - "but we didn't offer it!"); - return 1; - } - - if (ps->state == 6) { - int ret; - ret = proxy_socks5_selectchap(ps); - if (ret) return ret; - } - - } - - plug_closing_error(ps->plug, "Proxy error: Unexpected proxy error"); - return 1; -} - -/* ---------------------------------------------------------------------- - * `Telnet' proxy type. - * - * (This is for ad-hoc proxies where you connect to the proxy's - * telnet port and send a command such as `connect host port'. The - * command is configurable, since this proxy type is typically not - * standardised or at all well-defined.) - */ - -char *format_telnet_command(SockAddr *addr, int port, Conf *conf) -{ - char *fmt = conf_get_str(conf, CONF_proxy_telnet_command); - int so = 0, eo = 0; - strbuf *buf = strbuf_new(); - - /* we need to escape \\, \%, \r, \n, \t, \x??, \0???, - * %%, %host, %port, %user, and %pass - */ - - while (fmt[eo] != 0) { - - /* scan forward until we hit end-of-line, - * or an escape character (\ or %) */ - while (fmt[eo] != 0 && fmt[eo] != '%' && fmt[eo] != '\\') - eo++; - - /* if we hit eol, break out of our escaping loop */ - if (fmt[eo] == 0) break; - - /* if there was any unescaped text before the escape - * character, send that now */ - if (eo != so) - put_data(buf, fmt + so, eo - so); - - so = eo++; - - /* if the escape character was the last character of - * the line, we'll just stop and send it. */ - if (fmt[eo] == 0) break; - - if (fmt[so] == '\\') { - - /* we recognize \\, \%, \r, \n, \t, \x??. - * anything else, we just send unescaped (including the \). - */ - - switch (fmt[eo]) { - - case '\\': - put_byte(buf, '\\'); - eo++; - break; - - case '%': - put_byte(buf, '%'); - eo++; - break; - - case 'r': - put_byte(buf, '\r'); - eo++; - break; - - case 'n': - put_byte(buf, '\n'); - eo++; - break; - - case 't': - put_byte(buf, '\t'); - eo++; - break; - - case 'x': - case 'X': { - /* escaped hexadecimal value (ie. \xff) */ - unsigned char v = 0; - int i = 0; - - for (;;) { - eo++; - if (fmt[eo] >= '0' && fmt[eo] <= '9') - v += fmt[eo] - '0'; - else if (fmt[eo] >= 'a' && fmt[eo] <= 'f') - v += fmt[eo] - 'a' + 10; - else if (fmt[eo] >= 'A' && fmt[eo] <= 'F') - v += fmt[eo] - 'A' + 10; - else { - /* non hex character, so we abort and just - * send the whole thing unescaped (including \x) - */ - put_byte(buf, '\\'); - eo = so + 1; - break; - } - - /* we only extract two hex characters */ - if (i == 1) { - put_byte(buf, v); - eo++; - break; - } - - i++; - v <<= 4; - } - break; - } - - default: - put_data(buf, fmt + so, 2); - eo++; - break; - } - } else { - - /* % escape. we recognize %%, %host, %port, %user, %pass. - * %proxyhost, %proxyport. Anything else we just send - * unescaped (including the %). - */ - - if (fmt[eo] == '%') { - put_byte(buf, '%'); - eo++; - } - else if (strnicmp(fmt + eo, "host", 4) == 0) { - char dest[512]; - sk_getaddr(addr, dest, lenof(dest)); - put_data(buf, dest, strlen(dest)); - eo += 4; - } - else if (strnicmp(fmt + eo, "port", 4) == 0) { - put_fmt(buf, "%d", port); - eo += 4; - } - else if (strnicmp(fmt + eo, "user", 4) == 0) { - const char *username = conf_get_str(conf, CONF_proxy_username); - put_data(buf, username, strlen(username)); - eo += 4; - } - else if (strnicmp(fmt + eo, "pass", 4) == 0) { - const char *password = conf_get_str(conf, CONF_proxy_password); - put_data(buf, password, strlen(password)); - eo += 4; - } - else if (strnicmp(fmt + eo, "proxyhost", 9) == 0) { - const char *host = conf_get_str(conf, CONF_proxy_host); - put_data(buf, host, strlen(host)); - eo += 9; - } - else if (strnicmp(fmt + eo, "proxyport", 9) == 0) { - int port = conf_get_int(conf, CONF_proxy_port); - put_fmt(buf, "%d", port); - eo += 9; - } - else { - /* we don't escape this, so send the % now, and - * don't advance eo, so that we'll consider the - * text immediately following the % as unescaped. - */ - put_byte(buf, '%'); - } - } - - /* resume scanning for additional escapes after this one. */ - so = eo; - } - - /* if there is any unescaped text at the end of the line, send it */ - if (eo != so) { - put_data(buf, fmt + so, eo - so); - } - - return strbuf_to_str(buf); -} - -int proxy_telnet_negotiate (ProxySocket *ps, int change) -{ - if (ps->state == PROXY_CHANGE_NEW) { - char *formatted_cmd; - - formatted_cmd = format_telnet_command(ps->remote_addr, ps->remote_port, - ps->conf); - - { - /* - * Re-escape control chars in the command, for logging. - */ - char *reescaped = snewn(4*strlen(formatted_cmd) + 1, char); - const char *in; - char *out; - char *logmsg; - - for (in = formatted_cmd, out = reescaped; *in; in++) { - if (*in == '\n') { - *out++ = '\\'; *out++ = 'n'; - } else if (*in == '\r') { - *out++ = '\\'; *out++ = 'r'; - } else if (*in == '\t') { - *out++ = '\\'; *out++ = 't'; - } else if (*in == '\\') { - *out++ = '\\'; *out++ = '\\'; - } else if ((unsigned)(((unsigned char)*in) - 0x20) < - (0x7F-0x20)) { - *out++ = *in; - } else { - out += sprintf(out, "\\x%02X", (unsigned)*in & 0xFF); - } - } - *out = '\0'; - - logmsg = dupprintf("Sending Telnet proxy command: %s", reescaped); - plug_log(ps->plug, PLUGLOG_PROXY_MSG, NULL, 0, logmsg, 0); - sfree(logmsg); - sfree(reescaped); - } - - sk_write(ps->sub_socket, formatted_cmd, strlen(formatted_cmd)); - sfree(formatted_cmd); - - ps->state = 1; - return 0; - } - - if (change == PROXY_CHANGE_RECEIVE) { - /* we have received data from the underlying socket, which - * we'll need to parse, process, and respond to appropriately. - */ - - /* we're done */ - proxy_activate(ps); - /* proxy activate will have dealt with - * whatever is left of the buffer */ - return 1; - } - - plug_closing_error(ps->plug, "Proxy error: Unexpected proxy error"); - return 1; -} diff --git a/proxy/proxy.h b/proxy/proxy.h index 62aa144b..26ff20ef 100644 --- a/proxy/proxy.h +++ b/proxy/proxy.h @@ -11,6 +11,8 @@ #define PUTTY_PROXY_H typedef struct ProxySocket ProxySocket; +typedef struct ProxyNegotiator ProxyNegotiator; +typedef struct ProxyNegotiatorVT ProxyNegotiatorVT; struct ProxySocket { const char *error; @@ -25,59 +27,58 @@ struct ProxySocket { bufchain pending_input_data; bool pending_eof; -#define PROXY_STATE_NEW -1 -#define PROXY_STATE_ACTIVE 0 - - int state; /* proxy states greater than 0 are implementation - * dependent, but represent various stages/states - * of the initialization/setup/negotiation with the - * proxy server. - */ bool freeze; /* should we freeze the underlying socket when * we are done with the proxy negotiation? this * simply caches the value of sk_set_frozen calls. */ -#define PROXY_CHANGE_NEW -1 -#define PROXY_CHANGE_RECEIVE 2 - - /* something has changed (a call from the sub socket - * layer into our Proxy Plug layer, or we were just - * created, etc), so the proxy layer needs to handle - * this change (the type of which is the second argument) - * and further the proxy negotiation process. - */ - - int (*negotiate) (ProxySocket * /* this */, int /* change type */); - - /* current arguments of plug handlers - * (for use by proxy's negotiate function) - */ - - /* receive */ - bool receive_urgent; - const char *receive_data; - int receive_len; + ProxyNegotiator *pn; /* non-NULL if still negotiating */ + bufchain output_from_negotiator; /* configuration, used to look up proxy settings */ Conf *conf; - /* CHAP transient data */ - int chap_num_attributes; - int chap_num_attributes_processed; - int chap_current_attribute; - int chap_current_datalen; - Socket sock; Plug plugimpl; }; -extern void proxy_activate (ProxySocket *); +struct ProxyNegotiator { + const ProxyNegotiatorVT *vt; -extern int proxy_http_negotiate (ProxySocket *, int); -extern int proxy_telnet_negotiate (ProxySocket *, int); -extern int proxy_socks4_negotiate (ProxySocket *, int); -extern int proxy_socks5_negotiate (ProxySocket *, int); + /* Standard fields for any ProxyNegotiator. new() and free() don't + * have to set these up; that's done centrally, to save duplication. */ + ProxySocket *ps; + bufchain *input; + bufchain_sink output[1]; + + /* Set to report success during proxy negotiation. */ + bool done; + + /* Set to report an error during proxy negotiation. The main + * ProxySocket will free it, and will then guarantee never to call + * process_queue again. */ + char *error; +}; + +struct ProxyNegotiatorVT { + ProxyNegotiator *(*new)(const ProxyNegotiatorVT *); + void (*process_queue)(ProxyNegotiator *); + void (*free)(ProxyNegotiator *); + const char *type; +}; + +static inline ProxyNegotiator *proxy_negotiator_new( + const ProxyNegotiatorVT *vt) +{ return vt->new(vt); } +static inline void proxy_negotiator_process_queue(ProxyNegotiator *pn) +{ pn->vt->process_queue(pn); } +static inline void proxy_negotiator_free(ProxyNegotiator *pn) +{ pn->vt->free(pn); } + +extern const ProxyNegotiatorVT http_proxy_negotiator_vt; +extern const ProxyNegotiatorVT socks4_proxy_negotiator_vt; +extern const ProxyNegotiatorVT socks5_proxy_negotiator_vt; +extern const ProxyNegotiatorVT telnet_proxy_negotiator_vt; /* * This may be reused by local-command proxies on individual @@ -89,8 +90,7 @@ char *format_telnet_command(SockAddr *addr, int port, Conf *conf); * These are implemented in cproxy.c or nocproxy.c, depending on * whether encrypted proxy authentication is available. */ -extern void proxy_socks5_offerencryptedauth(BinarySink *); -extern int proxy_socks5_handlechap (ProxySocket *); -extern int proxy_socks5_selectchap(ProxySocket *); +extern const bool socks5_chap_available; +strbuf *chap_response(ptrlen challenge, ptrlen password); #endif diff --git a/proxy/socks4.c b/proxy/socks4.c new file mode 100644 index 00000000..ac85ec05 --- /dev/null +++ b/proxy/socks4.c @@ -0,0 +1,136 @@ +/* + * SOCKS 4 proxy negotiation. + */ + +#include "putty.h" +#include "network.h" +#include "proxy.h" +#include "socks.h" +#include "sshcr.h" + +typedef struct Socks4ProxyNegotiator { + int crLine; + ProxyNegotiator pn; +} Socks4ProxyNegotiator; + +static ProxyNegotiator *proxy_socks4_new(const ProxyNegotiatorVT *vt) +{ + Socks4ProxyNegotiator *s = snew(Socks4ProxyNegotiator); + s->pn.vt = vt; + s->crLine = 0; + return &s->pn; +} + +static void proxy_socks4_free(ProxyNegotiator *pn) +{ + Socks4ProxyNegotiator *s = container_of(pn, Socks4ProxyNegotiator, pn); + sfree(s); +} + +static void proxy_socks4_process_queue(ProxyNegotiator *pn) +{ + Socks4ProxyNegotiator *s = container_of(pn, Socks4ProxyNegotiator, pn); + + crBegin(s->crLine); + + { + char hostname[512]; + bool write_hostname = false; + + /* + * SOCKS 4 request packet: + * + * byte version + * byte command + * uint16 destination port number + * uint32 destination IPv4 address (or something in the + * SOCKS4A_NAME_FOLLOWS range) + * asciz username + * asciz destination hostname (if we sent SOCKS4A_NAME_FOLLOWS_*) + */ + + put_byte(pn->output, SOCKS4_REQUEST_VERSION); + put_byte(pn->output, SOCKS_CMD_CONNECT); + put_uint16(pn->output, pn->ps->remote_port); + + switch (sk_addrtype(pn->ps->remote_addr)) { + case ADDRTYPE_IPV4: { + char addr[4]; + sk_addrcopy(pn->ps->remote_addr, addr); + put_data(pn->output, addr, 4); + break; + } + case ADDRTYPE_NAME: + put_uint32(pn->output, SOCKS4A_NAME_FOLLOWS_BASE); + sk_getaddr(pn->ps->remote_addr, hostname, lenof(hostname)); + write_hostname = true; + break; + case ADDRTYPE_IPV6: + pn->error = dupstr("SOCKS version 4 does not support IPv6"); + crStopV; + } + + put_asciz(pn->output, conf_get_str(pn->ps->conf, CONF_proxy_username)); + + if (write_hostname) + put_asciz(pn->output, hostname); + } + + crReturnV; + + { + unsigned char data[8]; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, data, 8)); + + /* + * SOCKS 4 response packet: + * + * byte version + * byte status + * uint16 port number + * uint32 IPv4 address + * + * We don't need to worry about the port and destination address. + */ + + if (data[0] != SOCKS4_REPLY_VERSION) { + pn->error = dupprintf("SOCKS proxy response contained reply " + "version number %d (expected 0)", + (int)data[0]); + crStopV; + } + + switch (data[1]) { + case SOCKS4_RESP_SUCCESS: + pn->done = true; + break; + + case SOCKS4_RESP_FAILURE: + pn->error = dupstr("SOCKS server reported failure to connect"); + break; + + case SOCKS4_RESP_WANT_IDENTD: + pn->error = dupstr("SOCKS server wanted IDENTD on client"); + break; + + case SOCKS4_RESP_IDENTD_MISMATCH: + pn->error = dupstr("Username and IDENTD on client don't agree"); + break; + + default: + pn->error = dupprintf("SOCKS server sent unrecognised error " + "code %d", (int)data[1]); + break; + } + crStopV; + } + + crFinishV; +} + +const struct ProxyNegotiatorVT socks4_proxy_negotiator_vt = { + .new = proxy_socks4_new, + .free = proxy_socks4_free, + .process_queue = proxy_socks4_process_queue, + .type = "SOCKS 4", +}; diff --git a/proxy/socks5.c b/proxy/socks5.c new file mode 100644 index 00000000..407e98ea --- /dev/null +++ b/proxy/socks5.c @@ -0,0 +1,434 @@ +/* + * SOCKS 5 proxy negotiation. + */ + +#include "putty.h" +#include "network.h" +#include "proxy.h" +#include "socks.h" +#include "sshcr.h" + +static inline const char *socks5_auth_name(unsigned char m) +{ + switch (m) { + case SOCKS5_AUTH_NONE: return "none"; + case SOCKS5_AUTH_GSSAPI: return "GSSAPI"; + case SOCKS5_AUTH_PASSWORD: return "password"; + case SOCKS5_AUTH_CHAP: return "CHAP"; + default: return "unknown"; + } +} + +static inline const char *socks5_response_text(unsigned char m) +{ + switch (m) { + case SOCKS5_RESP_SUCCESS: return "success"; + case SOCKS5_RESP_FAILURE: return "unspecified failure"; + case SOCKS5_RESP_CONNECTION_NOT_ALLOWED_BY_RULESET: + return "connection not allowed by ruleset"; + case SOCKS5_RESP_NETWORK_UNREACHABLE: return "network unreachable"; + case SOCKS5_RESP_HOST_UNREACHABLE: return "host unreachable"; + case SOCKS5_RESP_CONNECTION_REFUSED: return "connection refused"; + case SOCKS5_RESP_TTL_EXPIRED: return "TTL expired"; + case SOCKS5_RESP_COMMAND_NOT_SUPPORTED: return "command not supported"; + case SOCKS5_RESP_ADDRTYPE_NOT_SUPPORTED: + return "address type not supported"; + default: return "unknown"; + } +} + +typedef struct Socks5ProxyNegotiator { + int crLine; + strbuf *auth_methods_offered; + unsigned char auth_method; + unsigned n_chap_attrs; + unsigned chap_attr, chap_attr_len; + unsigned char chap_buf[256]; + int response_addr_length; + ProxyNegotiator pn; +} Socks5ProxyNegotiator; + +static ProxyNegotiator *proxy_socks5_new(const ProxyNegotiatorVT *vt) +{ + Socks5ProxyNegotiator *s = snew(Socks5ProxyNegotiator); + memset(s, 0, sizeof(*s)); + s->pn.vt = vt; + s->auth_methods_offered = strbuf_new(); + return &s->pn; +} + +static void proxy_socks5_free(ProxyNegotiator *pn) +{ + Socks5ProxyNegotiator *s = container_of(pn, Socks5ProxyNegotiator, pn); + strbuf_free(s->auth_methods_offered); + smemclr(s, sizeof(s)); + sfree(s); +} + +static void proxy_socks5_process_queue(ProxyNegotiator *pn) +{ + Socks5ProxyNegotiator *s = container_of(pn, Socks5ProxyNegotiator, pn); + + crBegin(s->crLine); + + /* + * SOCKS 5 initial client packet: + * + * byte version + * byte number of available auth methods + * byte[] that many bytes indicating auth types + */ + + put_byte(pn->output, SOCKS5_REQUEST_VERSION); + + strbuf_clear(s->auth_methods_offered); + + put_byte(s->auth_methods_offered, SOCKS5_AUTH_NONE); + + { + const char *username = conf_get_str(pn->ps->conf, CONF_proxy_username); + const char *password = conf_get_str(pn->ps->conf, CONF_proxy_password); + if (username[0] || password[0]) { + if (socks5_chap_available) + put_byte(s->auth_methods_offered, SOCKS5_AUTH_CHAP); + + put_byte(s->auth_methods_offered, SOCKS5_AUTH_PASSWORD); + } + } + + put_byte(pn->output, s->auth_methods_offered->len); + put_datapl(pn->output, ptrlen_from_strbuf(s->auth_methods_offered)); + + crReturnV; + + /* + * SOCKS 5 initial server packet: + * + * byte version + * byte selected auth method, or SOCKS5_AUTH_REJECTED + */ + { + unsigned char data[2]; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, data, 2)); + + if (data[0] != SOCKS5_REPLY_VERSION) { + pn->error = dupprintf("SOCKS proxy returned unexpected " + "reply version %d (expected %d)", + (int)data[0], SOCKS5_REPLY_VERSION); + crStopV; + } + + if (data[1] == SOCKS5_AUTH_REJECTED) { + pn->error = dupstr("SOCKS server rejected every authentication " + "method we offered"); + crStopV; + } + + { + bool found = false; + for (size_t i = 0; i < s->auth_methods_offered->len; i++) + if (s->auth_methods_offered->u[i] == data[1]) { + found = true; + break; + } + + if (!found) { + pn->error = dupprintf("SOCKS server asked for auth method %d " + "(%s), which we did not offer", + (int)data[1], socks5_auth_name(data[1])); + crStopV; + } + } + + s->auth_method = data[1]; + } + + /* + * Process each auth method. Note we can't do this using the + * natural idiom of a switch statement, because there are + * crReturns inside some cases. + */ + if (s->auth_method == SOCKS5_AUTH_NONE) { + /* 'none' auth needs no further negotiation, so skip ahead + * to actually making the connection */ + goto authenticated; + } + + if (s->auth_method == SOCKS5_AUTH_PASSWORD) { + /* + * SOCKS 5 password auth packet: + * + * byte version + * pstring username + * pstring password + */ + const char *username = conf_get_str( + pn->ps->conf, CONF_proxy_username); + const char *password = conf_get_str( + pn->ps->conf, CONF_proxy_password); + put_byte(pn->output, SOCKS5_AUTH_PASSWORD_VERSION); + if (!put_pstring(pn->output, username)) { + pn->error = dupstr("SOCKS 5 authentication cannot support " + "usernames longer than 255 chars"); + crStopV; + } + if (!put_pstring(pn->output, password)) { + pn->error = dupstr("SOCKS 5 authentication cannot support " + "passwords longer than 255 chars"); + crStopV; + } + + /* + * SOCKS 5 password reply packet: + * + * byte version + * byte 0 for success, >0 for failure + */ + unsigned char data[2]; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, data, 2)); + + if (data[0] != SOCKS5_AUTH_PASSWORD_VERSION) { + pn->error = dupprintf( + "SOCKS 5 password reply had version number %d (expected " + "%d)", (int)data[0], SOCKS5_AUTH_PASSWORD_VERSION); + crStopV; + } + + if (data[1] != 0) { + pn->error = dupstr("SOCKS 5 server rejected our password"); + crStopV; + } + } else if (s->auth_method == SOCKS5_AUTH_CHAP) { + assert(socks5_chap_available); + + /* + * All CHAP packets, in both directions, have the same + * overall format: + * + * byte version + * byte number of attributes + * + * and then for each attribute: + * + * byte attribute type + * byte length + * byte[] that many bytes of payload + * + * In the initial outgoing packet we send two attributes: + * the list of supported algorithm names, and the + * username. + * + * (It's possible that we ought to delay sending the + * username until the second packet, in case the proxy + * sent back an attribute indicating which character set + * it would like us to use.) + */ + put_byte(pn->output, SOCKS5_AUTH_CHAP_VERSION); + put_byte(pn->output, 2); /* number of attributes */ + + put_byte(pn->output, SOCKS5_AUTH_CHAP_ATTR_ALGLIST); + put_byte(pn->output, 1); /* string length */ + put_byte(pn->output, SOCKS5_AUTH_CHAP_ALG_HMACMD5); + + /* Second attribute: username */ + { + const char *username = conf_get_str( + pn->ps->conf, CONF_proxy_username); + put_byte(pn->output, SOCKS5_AUTH_CHAP_ATTR_USERNAME); + if (!put_pstring(pn->output, username)) { + pn->error = dupstr( + "SOCKS 5 CHAP authentication cannot support " + "usernames longer than 255 chars"); + crStopV; + } + } + + while (true) { + /* + * Process a CHAP response packet, which has the same + * overall format as the outgoing packet shown above. + */ + unsigned char data[2]; + crMaybeWaitUntilV(bufchain_try_fetch_consume( + pn->input, data, 2)); + if (data[0] != SOCKS5_AUTH_CHAP_VERSION) { + pn->error = dupprintf( + "SOCKS 5 CHAP reply had version number %d (expected " + "%d)", (int)data[0], SOCKS5_AUTH_CHAP_VERSION); + crStopV; + } + + s->n_chap_attrs = data[1]; + if (s->n_chap_attrs == 0) { + /* + * If we receive a CHAP packet containing no + * attributes, then we have nothing we didn't have + * before, and can't make further progress. + */ + pn->error = dupprintf( + "SOCKS 5 CHAP reply sent no attributes"); + crStopV; + } + while (s->n_chap_attrs-- > 0) { + unsigned char data[2]; + crMaybeWaitUntilV(bufchain_try_fetch_consume( + pn->input, data, 2)); + s->chap_attr = data[0]; + s->chap_attr_len = data[1]; + crMaybeWaitUntilV(bufchain_try_fetch_consume( + pn->input, s->chap_buf, s->chap_attr_len)); + + if (s->chap_attr == SOCKS5_AUTH_CHAP_ATTR_STATUS) { + if (s->chap_attr_len == 1 && s->chap_buf[0] == 0) { + /* Status 0 means success: we are authenticated! */ + goto authenticated; + } else { + pn->error = dupstr( + "SOCKS 5 CHAP authentication failed"); + crStopV; + } + } else if (s->chap_attr==SOCKS5_AUTH_CHAP_ATTR_CHALLENGE) { + /* The CHAP challenge string. Send the response */ + const char *password = conf_get_str( + pn->ps->conf, CONF_proxy_password); + strbuf *response = chap_response( + make_ptrlen(s->chap_buf, s->chap_attr_len), + ptrlen_from_asciz(password)); + + put_byte(pn->output, SOCKS5_AUTH_CHAP_VERSION); + put_byte(pn->output, 1); /* number of attributes */ + put_byte(pn->output, SOCKS5_AUTH_CHAP_ATTR_RESPONSE); + put_byte(pn->output, response->len); + put_datapl(pn->output, ptrlen_from_strbuf(response)); + + strbuf_free(response); + } else { + /* ignore all other attributes */ + } + } + } + } else { + unreachable("bad auth method in SOCKS 5 negotiation"); + } + + authenticated: + + /* + * SOCKS 5 connection command: + * + * byte version + * byte command + * byte reserved (send as zero) + * byte address type + * byte[] address, with variable size (see below) + * uint16 port + */ + put_byte(pn->output, SOCKS5_REPLY_VERSION); + put_byte(pn->output, SOCKS_CMD_CONNECT); + put_byte(pn->output, 0); /* reserved byte */ + + switch (sk_addrtype(pn->ps->remote_addr)) { + case ADDRTYPE_IPV4: { + /* IPv4: address is 4 raw bytes */ + put_byte(pn->output, SOCKS5_ADDR_IPV4); + char buf[4]; + sk_addrcopy(pn->ps->remote_addr, buf); + put_data(pn->output, buf, sizeof(buf)); + break; + } + case ADDRTYPE_IPV6: { + /* IPv6: address is 16 raw bytes */ + put_byte(pn->output, SOCKS5_ADDR_IPV6); + char buf[16]; + sk_addrcopy(pn->ps->remote_addr, buf); + put_data(pn->output, buf, sizeof(buf)); + break; + } + case ADDRTYPE_NAME: { + /* Hostname: address is a pstring (Pascal-style string, + * unterminated but with a one-byte prefix length) */ + put_byte(pn->output, SOCKS5_ADDR_HOSTNAME); + char hostname[512]; + sk_getaddr(pn->ps->remote_addr, hostname, lenof(hostname)); + if (!put_pstring(pn->output, hostname)) { + pn->error = dupstr( + "SOCKS 5 cannot support host names longer than 255 chars"); + crStopV; + } + break; + } + default: + unreachable("Unexpected addrtype in SOCKS 5 proxy"); + } + + put_uint16(pn->output, pn->ps->remote_port); + crReturnV; + + /* + * SOCKS 5 connection response: + * + * byte version + * byte status + * byte reserved + * byte address type + * byte[] address bound to (same formats as in connection request) + * uint16 port + * + * We read the first four bytes and then decide what to do next. + */ + { + unsigned char data[4]; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, data, 4)); + + if (data[0] != SOCKS5_REPLY_VERSION) { + pn->error = dupprintf("SOCKS proxy returned unexpected " + "reply version %d (expected %d)", + (int)data[0], SOCKS5_REPLY_VERSION); + crStopV; + } + + if (data[1] != SOCKS5_RESP_SUCCESS) { + pn->error = dupprintf("SOCKS proxy failed to connect, error %d " + "(%s)", (int)data[1], + socks5_response_text(data[1])); + crStopV; + } + + /* + * Process each address type to find out the size of the rest + * of the packet. Note we can't do this using the natural + * idiom of a switch statement, because there are crReturns + * inside some cases. + */ + if (data[3] == SOCKS5_ADDR_IPV4) { + s->response_addr_length = 4; + } else if (data[3] == SOCKS5_ADDR_IPV6) { + s->response_addr_length = 16; + } else if (data[3] == SOCKS5_ADDR_HOSTNAME) { + /* Read the hostname length byte to find out how much to read */ + unsigned char len; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, &len, 1)); + s->response_addr_length = len; + break; + } else { + pn->error = dupprintf("SOCKS proxy response included unknown " + "address type %d", (int)data[3]); + crStopV; + } + + /* Read and ignore the address and port fields */ + crMaybeWaitUntilV(bufchain_try_consume( + pn->input, s->response_addr_length + 2)); + } + + /* And we're done! */ + pn->done = true; + crFinishV; +} + +const struct ProxyNegotiatorVT socks5_proxy_negotiator_vt = { + .new = proxy_socks5_new, + .free = proxy_socks5_free, + .process_queue = proxy_socks5_process_queue, + .type = "SOCKS 5", +}; diff --git a/proxy/telnet.c b/proxy/telnet.c new file mode 100644 index 00000000..91e6060c --- /dev/null +++ b/proxy/telnet.c @@ -0,0 +1,246 @@ +/* + * "Telnet" proxy negotiation. + * + * (This is for ad-hoc proxies where you connect to the proxy's + * telnet port and send a command such as `connect host port'. The + * command is configurable, since this proxy type is typically not + * standardised or at all well-defined.) + */ + +#include "putty.h" +#include "network.h" +#include "proxy.h" + +char *format_telnet_command(SockAddr *addr, int port, Conf *conf) +{ + char *fmt = conf_get_str(conf, CONF_proxy_telnet_command); + int so = 0, eo = 0; + strbuf *buf = strbuf_new(); + + /* we need to escape \\, \%, \r, \n, \t, \x??, \0???, + * %%, %host, %port, %user, and %pass + */ + + while (fmt[eo] != 0) { + + /* scan forward until we hit end-of-line, + * or an escape character (\ or %) */ + while (fmt[eo] != 0 && fmt[eo] != '%' && fmt[eo] != '\\') + eo++; + + /* if we hit eol, break out of our escaping loop */ + if (fmt[eo] == 0) break; + + /* if there was any unescaped text before the escape + * character, send that now */ + if (eo != so) + put_data(buf, fmt + so, eo - so); + + so = eo++; + + /* if the escape character was the last character of + * the line, we'll just stop and send it. */ + if (fmt[eo] == 0) break; + + if (fmt[so] == '\\') { + + /* we recognize \\, \%, \r, \n, \t, \x??. + * anything else, we just send unescaped (including the \). + */ + + switch (fmt[eo]) { + + case '\\': + put_byte(buf, '\\'); + eo++; + break; + + case '%': + put_byte(buf, '%'); + eo++; + break; + + case 'r': + put_byte(buf, '\r'); + eo++; + break; + + case 'n': + put_byte(buf, '\n'); + eo++; + break; + + case 't': + put_byte(buf, '\t'); + eo++; + break; + + case 'x': + case 'X': { + /* escaped hexadecimal value (ie. \xff) */ + unsigned char v = 0; + int i = 0; + + for (;;) { + eo++; + if (fmt[eo] >= '0' && fmt[eo] <= '9') + v += fmt[eo] - '0'; + else if (fmt[eo] >= 'a' && fmt[eo] <= 'f') + v += fmt[eo] - 'a' + 10; + else if (fmt[eo] >= 'A' && fmt[eo] <= 'F') + v += fmt[eo] - 'A' + 10; + else { + /* non hex character, so we abort and just + * send the whole thing unescaped (including \x) + */ + put_byte(buf, '\\'); + eo = so + 1; + break; + } + + /* we only extract two hex characters */ + if (i == 1) { + put_byte(buf, v); + eo++; + break; + } + + i++; + v <<= 4; + } + break; + } + + default: + put_data(buf, fmt + so, 2); + eo++; + break; + } + } else { + + /* % escape. we recognize %%, %host, %port, %user, %pass. + * %proxyhost, %proxyport. Anything else we just send + * unescaped (including the %). + */ + + if (fmt[eo] == '%') { + put_byte(buf, '%'); + eo++; + } + else if (strnicmp(fmt + eo, "host", 4) == 0) { + char dest[512]; + sk_getaddr(addr, dest, lenof(dest)); + put_data(buf, dest, strlen(dest)); + eo += 4; + } + else if (strnicmp(fmt + eo, "port", 4) == 0) { + put_fmt(buf, "%d", port); + eo += 4; + } + else if (strnicmp(fmt + eo, "user", 4) == 0) { + const char *username = conf_get_str(conf, CONF_proxy_username); + put_data(buf, username, strlen(username)); + eo += 4; + } + else if (strnicmp(fmt + eo, "pass", 4) == 0) { + const char *password = conf_get_str(conf, CONF_proxy_password); + put_data(buf, password, strlen(password)); + eo += 4; + } + else if (strnicmp(fmt + eo, "proxyhost", 9) == 0) { + const char *host = conf_get_str(conf, CONF_proxy_host); + put_data(buf, host, strlen(host)); + eo += 9; + } + else if (strnicmp(fmt + eo, "proxyport", 9) == 0) { + int port = conf_get_int(conf, CONF_proxy_port); + put_fmt(buf, "%d", port); + eo += 9; + } + else { + /* we don't escape this, so send the % now, and + * don't advance eo, so that we'll consider the + * text immediately following the % as unescaped. + */ + put_byte(buf, '%'); + } + } + + /* resume scanning for additional escapes after this one. */ + so = eo; + } + + /* if there is any unescaped text at the end of the line, send it */ + if (eo != so) { + put_data(buf, fmt + so, eo - so); + } + + return strbuf_to_str(buf); +} + +typedef struct TelnetProxyNegotiator { + ProxyNegotiator pn; +} TelnetProxyNegotiator; + +static ProxyNegotiator *proxy_telnet_new(const ProxyNegotiatorVT *vt) +{ + TelnetProxyNegotiator *s = snew(TelnetProxyNegotiator); + s->pn.vt = vt; + return &s->pn; +} + +static void proxy_telnet_free(ProxyNegotiator *pn) +{ + TelnetProxyNegotiator *s = container_of(pn, TelnetProxyNegotiator, pn); + sfree(s); +} + +static void proxy_telnet_process_queue(ProxyNegotiator *pn) +{ + // TelnetProxyNegotiator *s = container_of(pn, TelnetProxyNegotiator, pn); + + char *formatted_cmd = format_telnet_command( + pn->ps->remote_addr, pn->ps->remote_port, pn->ps->conf); + + /* + * Re-escape control chars in the command, for logging. + */ + strbuf *logmsg = strbuf_new(); + const char *in; + + put_datapl(logmsg, PTRLEN_LITERAL("Sending Telnet proxy command: ")); + + for (in = formatted_cmd; *in; in++) { + if (*in == '\n') { + put_datapl(logmsg, PTRLEN_LITERAL("\\n")); + } else if (*in == '\r') { + put_datapl(logmsg, PTRLEN_LITERAL("\\r")); + } else if (*in == '\t') { + put_datapl(logmsg, PTRLEN_LITERAL("\\t")); + } else if (*in == '\\') { + put_datapl(logmsg, PTRLEN_LITERAL("\\\\")); + } else if (0x20 <= *in && *in < 0x7F) { + put_byte(logmsg, *in); + } else { + put_fmt(logmsg, "\\x%02X", (unsigned)*in & 0xFF); + } + } + + plug_log(pn->ps->plug, PLUGLOG_PROXY_MSG, NULL, 0, logmsg->s, 0); + strbuf_free(logmsg); + + put_dataz(pn->output, formatted_cmd); + sfree(formatted_cmd); + + /* + * Unconditionally report success. + */ + pn->done = true; +} + +const struct ProxyNegotiatorVT telnet_proxy_negotiator_vt = { + .new = proxy_telnet_new, + .free = proxy_telnet_free, + .process_queue = proxy_telnet_process_queue, + .type = "Telnet", +};