proxy.c 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. /*
  2. * sapphire-backend
  3. *
  4. * Copyright (C) 2018 Alyssa Rosenzweig
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 2 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program; if not, write to the Free Software
  18. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
  19. *
  20. */
  21. #include <stdio.h>
  22. #include <stdint.h>
  23. #include <assert.h>
  24. #include <glib.h>
  25. #include <libsoup/soup.h>
  26. #include <signal.h>
  27. #include <string.h>
  28. #ifndef _WIN32
  29. #include <unistd.h>
  30. #else
  31. #include "win32/win32dep.h"
  32. #endif
  33. #include <fcntl.h>
  34. #include <gio/gio.h>
  35. #include <gio/gunixsocketaddress.h>
  36. #include "json_compat.h"
  37. /* Represents a connection to a given client. Minimal state should be kept
  38. * here, since connections are device-specific, not for the client as a whole.
  39. * Essentially, just enough for the websocket metadata and a little potpourrie */
  40. typedef struct Connection {
  41. /* The reference counted connection itself */
  42. SoupWebsocketConnection *connection;
  43. /* The reference counted proxied connection */
  44. GSocketConnection *proxy_connection;
  45. GDataInputStream *distream;
  46. /* Has this connection authenticated yet? */
  47. gboolean is_authenticated;
  48. /* Remote IP Address. Must be g_free'd on destroy */
  49. gchar *ip_address;
  50. } Connection;
  51. static void
  52. sapphire_close_connection(Connection *conn);
  53. /* Whether this proxy supports multi-user mode */
  54. static gboolean multi_user = FALSE;
  55. /* Helper to serialize and broadcast */
  56. #define WS_PORT 7070
  57. /* List of all authenticated connections. These connections will be broadcasted
  58. * to by broadcast_raw_packet. */
  59. GList *authenticated_connections = NULL;
  60. static void
  61. sapphire_send_raw_packet(Connection *conn, const char *packet)
  62. {
  63. if (soup_websocket_connection_get_state(conn->connection) != SOUP_WEBSOCKET_STATE_OPEN) {
  64. printf("Ignoring write to closed websocket\n");
  65. return;
  66. }
  67. soup_websocket_connection_send_text(conn->connection, packet);
  68. }
  69. static void
  70. sapphire_send(Connection *conn, JsonObject *msg)
  71. {
  72. gchar *str = json_object_to_string(msg);
  73. sapphire_send_raw_packet(conn, str);
  74. g_free(str);
  75. }
  76. static JsonNode *
  77. json_parse_to_root(gchar *frame)
  78. {
  79. JsonParser *parser = json_parser_new();
  80. if (!json_parser_load_from_data(parser, frame, -1, NULL)) {
  81. fprintf(stderr, "Error parsing response: %s\n", frame);
  82. return NULL;
  83. }
  84. return json_parser_get_root(parser);
  85. }
  86. /* Serialize accounts to/from disk. TODO: Serializing back */
  87. typedef struct {
  88. gchar *password_hash;
  89. } SapphireAccount;
  90. GHashTable *username_to_account;
  91. static void
  92. sapphire_load_accounts(const gchar *name)
  93. {
  94. GError *error = NULL;
  95. gchar *contents = NULL;
  96. gboolean success = g_file_get_contents(name, &contents, NULL, &error);
  97. if (!success) {
  98. printf("Bad read\n");
  99. exit(1);
  100. }
  101. JsonNode *root = json_parse_to_root(contents);
  102. g_free(contents);
  103. if (root == NULL) {
  104. printf("NULL root, ignoring\n");
  105. return;
  106. }
  107. JsonArray *accounts = json_node_get_array(root);
  108. int len = json_array_get_length(accounts);
  109. for (int i = 0; i < len; ++i) {
  110. JsonObject *account = json_array_get_object_element(accounts, i);
  111. const gchar *name = json_object_get_string_member(account, "name");
  112. SapphireAccount *sapph = g_new0(SapphireAccount, 1);
  113. sapph->password_hash = g_strdup(json_object_get_string_member(account, "passwordHash"));
  114. g_hash_table_insert(username_to_account, g_strdup(name), sapph);
  115. }
  116. if (len > 1 && !multi_user) {
  117. printf("ERROR: Multi-user mode not enabled (did you enable TLS?) but multiple accounts in the database\n");
  118. exit(1);
  119. }
  120. }
  121. /* Functions for icon proxying, TODO segregate by user */
  122. GHashTable *username_to_icon;
  123. /* Generic container for icons to paper over the difference between
  124. * PurpleStoredImage and PurpleBuddyIcon */
  125. typedef struct {
  126. const gchar *extension;
  127. size_t size;
  128. gconstpointer data;
  129. } SapphireIcon;
  130. static void
  131. soup_icon_callback(SoupServer *server,
  132. SoupMessage *msg,
  133. const char *path,
  134. GHashTable *query,
  135. SoupClientContext *client,
  136. gpointer user_data)
  137. {
  138. if (msg->method != SOUP_METHOD_GET) {
  139. soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
  140. return;
  141. }
  142. /* Extract the name from the query */
  143. if (!g_hash_table_contains(query, "name")) {
  144. soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
  145. return;
  146. }
  147. const gchar *name = g_hash_table_lookup(query, "name");
  148. /* Search for icon */
  149. SapphireIcon *icon = g_hash_table_lookup(username_to_icon, name);
  150. if (!icon) {
  151. soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
  152. return;
  153. }
  154. /* We found it, so return the icon appropriately */
  155. gchar *mimetype = g_strdup_printf("image/%s", icon->extension);
  156. if (!icon->data) {
  157. soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
  158. return;
  159. }
  160. soup_message_set_status(msg, SOUP_STATUS_OK);
  161. soup_message_set_response(msg, mimetype, SOUP_MEMORY_TEMPORARY, icon->data, icon->size);
  162. /* Set cacheing header */
  163. soup_message_headers_append(msg->response_headers, "Cache-Control", "public, max-age=2592000");
  164. g_free(mimetype);
  165. }
  166. /* IP -> rate limit hash table. Rate limits are integers of the number of
  167. * seconds rate limited. If positive, the rate limit is active. If negative, it
  168. * is inactive and merely stored for posterity. Rate limits are mostly reset on
  169. * a successful authentication */
  170. GHashTable *rate_limits;
  171. /* Callback for a timer once the rate limit is restored for the next attempt */
  172. static gboolean
  173. sapphire_restore_rate_limit(gpointer user_data)
  174. {
  175. gchar *ip_address = (gchar *) user_data;
  176. int limit = abs(GPOINTER_TO_INT(g_hash_table_lookup(rate_limits, ip_address)));
  177. g_hash_table_replace(rate_limits, ip_address, GINT_TO_POINTER(-limit));
  178. /* We can't free the ip address yet, since it still needs it for the
  179. * key. TODO: How not to leak? */
  180. return FALSE;
  181. }
  182. static void
  183. sapphire_got_internal(gchar *frame)
  184. {
  185. JsonNode *root = json_parse_to_root(frame);
  186. if (root == NULL) {
  187. printf("NULL root, ignoring\n");
  188. return;
  189. }
  190. JsonObject *obj = json_node_get_object(root);
  191. const gchar *op = json_object_get_string_member(obj, "op");
  192. if (g_strcmp0(op, "icon") == 0) {
  193. SapphireIcon *icon = g_new0(SapphireIcon, 1);
  194. const gchar *name = json_object_get_string_member(obj, "name");
  195. const gchar *base64 = json_object_get_string_member(obj, "base64");
  196. icon->extension = json_object_get_string_member(obj, "ext");
  197. icon->data = g_base64_decode(base64, &icon->size);
  198. g_hash_table_insert(username_to_icon, g_strdup(name), icon);
  199. } else {
  200. printf("Unknown op %s\n", op);
  201. return;
  202. }
  203. }
  204. static void sapphire_got_line(GObject *source_object, GAsyncResult *res, gpointer user_data);
  205. static void
  206. sapphire_read_line(Connection *conn)
  207. {
  208. g_data_input_stream_read_line_async(conn->distream, G_PRIORITY_DEFAULT, NULL, sapphire_got_line, conn);
  209. }
  210. static void
  211. sapphire_got_line(GObject *source_object,
  212. GAsyncResult *res,
  213. gpointer user_data)
  214. {
  215. Connection *conn = (Connection *) user_data;
  216. GError *err = NULL;
  217. gsize len;
  218. char *data = g_data_input_stream_read_line_finish_utf8(G_DATA_INPUT_STREAM(source_object), res, &len, &err);
  219. if (err || !data) {
  220. /* Borp, error -- disconnect */
  221. printf("Disconnecting %s...\n", err ? err->message : "");
  222. if (data)
  223. g_free(data);
  224. sapphire_close_connection(conn);
  225. return;
  226. }
  227. /* Check the first character. If it's >, this is an internal packet. Otherwise, pass it along */
  228. if (data[0] == '>') {
  229. sapphire_got_internal(data + 1);
  230. } else {
  231. sapphire_send_raw_packet(conn, data);
  232. }
  233. g_free(data);
  234. sapphire_read_line(conn);
  235. }
  236. static gboolean
  237. sapphire_try_login(Connection *conn, const char *username, const char *attempted_hash);
  238. static void
  239. sapphire_send_rate_limit(Connection *conn, int ms);
  240. static void
  241. soup_ws_data(SoupWebsocketConnection *self,
  242. gint type,
  243. GBytes *message,
  244. gpointer user_data)
  245. {
  246. struct Connection *conn = (struct Connection *) user_data;
  247. const gchar *frame = (const gchar *) g_bytes_get_data(message, NULL);
  248. /* The message should be interpreted as JSON, decode that here */
  249. JsonParser *parser = json_parser_new();
  250. if (!json_parser_load_from_data(parser, frame, -1, NULL)) {
  251. fprintf(stderr, "Error parsing response: ...\n");
  252. return;
  253. }
  254. JsonNode *root = json_parser_get_root(parser);
  255. if (root == NULL) {
  256. printf("NULL root, ignoring\n");
  257. return;
  258. }
  259. /* How to proceed depends if we're authenticated or not. If we are,
  260. * this is a standard client message, ready to be parsed, relayed, and
  261. * actuated. If we are not, this is an authentication message (by
  262. * definition -- otherwise they get booted to penalize credential
  263. * attacks */
  264. if (conn->is_authenticated) {
  265. /* Forward the authenticated packet */
  266. if (!conn->proxy_connection) {
  267. printf("No proxy\n");
  268. return;
  269. }
  270. GError *gerror;
  271. GOutputStream *ostream = g_io_stream_get_output_stream (G_IO_STREAM (conn->proxy_connection));
  272. g_output_stream_write_all(ostream, frame, strlen(frame), NULL, NULL, &gerror);
  273. char end = '\n';
  274. g_output_stream_write(ostream, &end, 1, NULL, &gerror);
  275. } else {
  276. JsonObject *obj = json_node_get_object(root);
  277. gboolean success = FALSE;
  278. const char *username;
  279. if (obj) {
  280. username = json_object_get_string_member(obj, "username");
  281. const char *passwordHash = json_object_get_string_member(obj, "passwordHash");
  282. if (username && passwordHash) {
  283. success = sapphire_try_login(conn, username, passwordHash);
  284. }
  285. }
  286. if (!success) {
  287. /* Slow down future attempts for rate limiting */
  288. int limit = abs(GPOINTER_TO_INT(g_hash_table_lookup(rate_limits, conn->ip_address)));
  289. if (limit == 0) {
  290. /* Initial rate limit of 320ms, grows exponentially by two's */
  291. limit = 1;
  292. }
  293. /* Exponential rate limit growth for repeat offenders */
  294. limit *= 2;
  295. /* Store that limit */
  296. g_hash_table_insert(rate_limits, g_strdup(conn->ip_address), GINT_TO_POINTER(limit));
  297. /* Create a timeout to restore their access */
  298. int milliseconds = limit * 160;
  299. g_timeout_add(milliseconds, sapphire_restore_rate_limit, g_strdup(conn->ip_address));
  300. /* Tell the client how long we're rate limiting them for */
  301. sapphire_send_rate_limit(conn, milliseconds);
  302. /* Eject */
  303. const char *error = "Authentication error";
  304. soup_websocket_connection_close(conn->connection, SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION, error);
  305. return;
  306. } else {
  307. /* Successful login - so get rid of the rate limit */
  308. g_hash_table_replace(rate_limits, g_strdup(conn->ip_address), GINT_TO_POINTER(0));
  309. }
  310. /* To authenticate, set the flag and add us to the list */
  311. conn->is_authenticated = TRUE;
  312. /* Remove size limit -- needed for avatar upload, etc. TODO: Is this risky? */
  313. soup_websocket_connection_set_max_incoming_payload_size(conn->connection, 0);
  314. authenticated_connections = g_list_append(authenticated_connections, conn);
  315. /* ...and connect to the appropriate backend's socket */
  316. gchar *socket_path = multi_user ? g_strdup_printf("./accounts/%s/sockpuppet", username) : g_strdup("./sockpuppet");
  317. GSocketClient *client = g_socket_client_new();
  318. GSocketAddress *addr = g_unix_socket_address_new(socket_path);
  319. GSocketConnection *proxy_conn = g_socket_client_connect(client, G_SOCKET_CONNECTABLE(addr), NULL, NULL);
  320. if (!proxy_conn) {
  321. fprintf(stderr, "Failed to proxy\n");
  322. JsonObject *data = json_object_new();
  323. json_object_set_string_member(data, "op", "proxyerror");
  324. sapphire_send(conn, data);
  325. json_object_unref(data);
  326. return;
  327. }
  328. conn->proxy_connection = g_object_ref(proxy_conn);
  329. GInputStream *istream = g_io_stream_get_input_stream (G_IO_STREAM (conn->proxy_connection));
  330. conn->distream = g_data_input_stream_new(istream);
  331. /* Start the async */
  332. sapphire_read_line(conn);
  333. }
  334. }
  335. static void
  336. soup_ws_error(SoupWebsocketConnection *self,
  337. GError *gerror,
  338. gpointer user_data)
  339. {
  340. fprintf(stderr, "WS Error\n");
  341. }
  342. static void
  343. sapphire_close_connection(Connection *conn)
  344. {
  345. /* Free connection */
  346. if (conn->ip_address) {
  347. g_free(conn->ip_address);
  348. conn->ip_address = NULL;
  349. }
  350. /* Disconnect the proxy-half of the connection */
  351. if (conn->proxy_connection) {
  352. g_io_stream_close(G_IO_STREAM(conn->proxy_connection), NULL, NULL);
  353. conn->proxy_connection = NULL;
  354. }
  355. /* Splice the socket out of the authenticated list, so we no longer
  356. * attempt to broadcast to it */
  357. authenticated_connections = g_list_remove(authenticated_connections, conn);
  358. }
  359. static void
  360. soup_ws_closed(SoupWebsocketConnection *self,
  361. gpointer user_data)
  362. {
  363. Connection *conn = (Connection *) user_data;
  364. sapphire_close_connection(conn);
  365. }
  366. static void
  367. soup_ws_callback(SoupServer *server,
  368. SoupWebsocketConnection *connection,
  369. const char *path,
  370. SoupClientContext *client,
  371. gpointer user_data)
  372. {
  373. /* Figure out who we're talking to */
  374. GSocketAddress *socket_address = soup_client_context_get_remote_address(client);
  375. GSocketFamily family = g_socket_address_get_family(socket_address);
  376. if ((family != G_SOCKET_FAMILY_IPV4) && (family != G_SOCKET_FAMILY_IPV6)) {
  377. /* Should be unreachable */
  378. fprintf(stderr, "Non-IP socket?\n");
  379. return;
  380. }
  381. GInetAddress *inet_address = g_inet_socket_address_get_address((GInetSocketAddress *) socket_address);
  382. gchar *addr = g_inet_address_to_string(inet_address);
  383. /* Allocate a connection object for us and fill it in */
  384. Connection *conn = g_new0(Connection, 1);
  385. conn->is_authenticated = FALSE;
  386. /* Save the connection.
  387. * IMPORTANT: Reference counting is necessary to keep the connection
  388. * alive. No idea why this isn't documented anywhere, xxx
  389. */
  390. conn->connection = g_object_ref(connection);
  391. /* Save the IP for ratelimiting */
  392. conn->ip_address = addr;
  393. /* Check for rate limiting, for that matter */
  394. int rate_limit = GPOINTER_TO_INT(g_hash_table_lookup(rate_limits, conn->ip_address));
  395. if (rate_limit > 0) {
  396. /* Violation -- eject */
  397. const char *error = "Rate limit violation";
  398. soup_websocket_connection_close(conn->connection, SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION, error);
  399. printf("Ejecting rate limit violation from %s\n", conn->ip_address);
  400. return;
  401. }
  402. /* Subscribe to the various signals */
  403. g_signal_connect(connection, "message", G_CALLBACK(soup_ws_data), conn);
  404. g_signal_connect(connection, "closed", G_CALLBACK(soup_ws_closed), conn);
  405. g_signal_connect(connection, "error", G_CALLBACK(soup_ws_error), conn);
  406. }
  407. static void
  408. sapphire_init_websocket(gboolean secure)
  409. {
  410. GError *error = NULL;
  411. SoupServer *soup = soup_server_new(NULL, NULL);
  412. if (secure) {
  413. gchar *key_file = "key.pem";
  414. gchar *cert_file = "cert.pem";
  415. if (!soup_server_set_ssl_cert_file(soup, cert_file, key_file, &error)) {
  416. printf("Error setting SSL certificate\n");
  417. printf("Msg: %s\n", error->message);
  418. exit(1);
  419. }
  420. } else {
  421. /* Be a little scary */
  422. fprintf(stderr, "* * *\n\nWARNING: RUNNING IN PLAIN HTTP MODE!!! DO NOT DEPLOY IN PRODUCTION!!!\n\n* * *\n\n");
  423. }
  424. /* Initialize icon endpoint */
  425. soup_server_add_handler(soup, "/icon/", soup_icon_callback, NULL, NULL);
  426. /* WebSocket entrypoint */
  427. char *protocols[] = { "binary", NULL };
  428. soup_server_add_websocket_handler(soup, "/ws", NULL, protocols, soup_ws_callback, NULL, NULL);
  429. if (!soup_server_listen_all(soup, WS_PORT, secure ? SOUP_SERVER_LISTEN_HTTPS : 0, &error)) {
  430. fprintf(stderr, "Error listening in soup\n");
  431. fprintf(stderr, "Msg: %s\n", error->message);
  432. exit(1);
  433. }
  434. }
  435. #include "secure-compare-64.h"
  436. static gboolean
  437. sapphire_try_login(Connection *conn, const char *username, const char *attempted_hash)
  438. {
  439. gboolean success = FALSE;
  440. /* Find the appropriate account */
  441. SapphireAccount *account = g_hash_table_lookup(username_to_account, username);
  442. if (!account) {
  443. printf("Bad account\n");
  444. return FALSE;
  445. }
  446. /* Valid hashes must be 64 bytes (32 bytes of binary -> 64 of hex) */
  447. if (strlen(attempted_hash) == 64 && strlen(account->password_hash) == 64) {
  448. /* Fetch the SHA-256 secret */
  449. success = secure_compare_64(attempted_hash, account->password_hash);
  450. }
  451. /* TODO: Rate limit on failure */
  452. /* Packet indicating status */
  453. if (success) {
  454. JsonObject *data = json_object_new();
  455. json_object_set_string_member(data, "op", "authsuccess");
  456. sapphire_send(conn, data);
  457. json_object_unref(data);
  458. }
  459. return success;
  460. }
  461. static void
  462. sapphire_send_rate_limit(Connection *conn, int ms)
  463. {
  464. JsonObject *data = json_object_new();
  465. json_object_set_string_member(data, "op", "ratelimit");
  466. json_object_set_int_member(data, "milliseconds", ms);
  467. sapphire_send(conn, data);
  468. json_object_unref(data);
  469. }
  470. /* A buddy joined in a room we're subscribed to -- but that doesn't mean the
  471. * client needs to know. Only send the joined event to clients that have opened
  472. * the corresponding conversation */
  473. GList *authenticated_connections;
  474. int main(int argc, char *argv[])
  475. {
  476. GMainLoop *loop;
  477. #ifdef _WIN32
  478. g_thread_init(NULL);
  479. #endif
  480. g_set_prgname("Sapphire-Proxy");
  481. g_set_application_name("Sapphire-Proxy");
  482. loop = g_main_loop_new(NULL, FALSE);
  483. g_main_loop_ref(loop);
  484. /* Initialize icon database */
  485. username_to_icon = g_hash_table_new(g_str_hash, g_str_equal);
  486. rate_limits = g_hash_table_new(g_str_hash, g_str_equal);
  487. username_to_account = g_hash_table_new(g_str_hash, g_str_equal);
  488. gboolean secure = (argc >= 2) && (g_strcmp0(argv[1], "--production") == 0);
  489. /* Only allow multi-user mode if we're TLS-encrypted */
  490. multi_user = secure;
  491. const gchar *database_path = (argc >= 3) ? argv[2] : "./sapphire-accounts.json";
  492. sapphire_load_accounts(database_path);
  493. sapphire_init_websocket(secure);
  494. g_main_context_iteration(g_main_loop_get_context(loop), FALSE);
  495. g_main_loop_run(loop);
  496. return 0;
  497. }