summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Whitton <spwhitton@spwhitton.name>2019-01-24 11:19:22 -0700
committerSean Whitton <spwhitton@spwhitton.name>2019-01-24 11:19:22 -0700
commit52980611d50985d60ef0d2d8baa37a679b95851c (patch)
treee4101ecdcf029c40c4952eae093a79cc8b6d1a29
parent9da3c2cef32bc16a6106b1276ccf2be30efe883f (diff)
parentfcf1a1e81d1853c329035012a90129b13bb8819d (diff)
Merge tag 'v1.2.0'
-rw-r--r--ChangeLog5
-rw-r--r--README44
-rw-r--r--configure.ac5
-rw-r--r--facebook/facebook-api.c326
-rw-r--r--facebook/facebook-api.h115
-rw-r--r--facebook/facebook-data.c8
-rw-r--r--facebook/facebook-mqtt.c19
-rw-r--r--facebook/facebook-util.c43
-rw-r--r--facebook/facebook-util.h26
-rw-r--r--facebook/facebook.c47
10 files changed, 602 insertions, 36 deletions
diff --git a/ChangeLog b/ChangeLog
index f197571..96edc0d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,8 @@
+bitlbee-facebook-1.2.0 (2019-01-24):
+ - Fix ERROR_QUEUE_OVERFLOW on login by bumping orca agent version
+ - Fix "Failed to read fixed header" with TLS 1.3 / GnuTLS 3.6.x
+ - Add workplace chat support (enable the "work" setting to use it)
+
bitlbee-facebook-1.1.2 (2017-08-30):
- Fix "Failed to read thrift" with unknown fields in /t_p payload
- Fix rare login hang/timeout when the last page of contacts is empty
diff --git a/README b/README
index 007b77d..2a12ba0 100644
--- a/README
+++ b/README
@@ -9,26 +9,58 @@ General usage instructions are available in the bitlbee wiki:
https://wiki.bitlbee.org/HowtoFacebookMQTT
-## Installing from APT repo
+## Installing with packages
+
+### Debian/ubuntu APT repo
An APT repo for several recent debian/ubuntu versions is available here:
https://jgeboski.github.io/
+This builds git/development versions.
+
+### Debian buster/backports
+
+Debian's official repos have packages for releases of this plugin, with the
+slightly different name "bitlbee-plugin-facebook". Use the APT repo if it's not
+the latest.
+
+ $ apt install bitlbee-plugin-facebook
+
+### Fedora
+
+ $ dnf install bitlbee-facebook
+
+### RHEL/CentOS
+
+Follow the general instructions for enabling EPEL before installing it:
+
+http://fedoraproject.org/wiki/EPEL#How_can_I_use_these_extra_packages.3F
+
+ $ yum install bitlbee-facebook
+
## Building from source
+The following packages are required: autoconf, automake, libtool, glib2,
+json-glib, bitlbee (names may vary across distros)
+
+Example for debian-based systems:
+
+ apt install build-essential autoconf automake libtool libglib2.0-dev libjson-glib-dev bitlbee-dev
+
+Example for Fedora-based systems:
+
+ dnf install gcc autoconf automake libtool glib2-devel json-glib-devel bitlbee-devel
+
Make sure bitlbee and its headers have been installed. If bitlbee came
from the distribution's repository, it will most likely need the
-development package, usually bitlbee-dev.
+development package, like bitlbee-dev or bitlbee-devel in the example
+above.
If bitlbee was built by hand (or alike via a script), ensure the make
target `install-dev` is invoked. This target is not called by default,
and will install the headers that are needed.
-Do *not* use the source tree headers unless you know what you are
-doing. This can lead to mismatched header versions, which often times
-will lead to bad things.
-
$ git clone https://github.com/bitlbee/bitlbee-facebook.git
$ cd bitlbee-facebook
diff --git a/configure.ac b/configure.ac
index fc31a95..d172aac 100644
--- a/configure.ac
+++ b/configure.ac
@@ -15,7 +15,7 @@
AC_INIT(
[bitlbee-facebook],
- [1.1.2],
+ [1.2.0],
[https://github.com/bitlbee/bitlbee-facebook/issues],
[bitlbee-facebook],
[https://github.com/bitlbee/bitlbee-facebook],
@@ -29,8 +29,7 @@ AM_INIT_AUTOMAKE([no-define])
AC_PROG_CC
AM_PROG_CC_C_O
-AC_DISABLE_STATIC
-AC_PROG_LIBTOOL
+LT_INIT([disable-static])
m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
m4_ifdef([AC_PROG_CC_C99], [AC_PROG_CC_C99])
diff --git a/facebook/facebook-api.c b/facebook/facebook-api.c
index fb0581d..16bbf6e 100644
--- a/facebook/facebook-api.c
+++ b/facebook/facebook-api.c
@@ -27,6 +27,7 @@
#include "facebook-util.h"
typedef struct _FbApiData FbApiData;
+typedef struct _FbApiPreloginData FbApiPreloginData;
enum
{
@@ -39,6 +40,7 @@ enum
PROP_TOKEN,
PROP_UID,
PROP_TWEAK,
+ PROP_WORK,
PROP_N
};
@@ -64,6 +66,10 @@ struct _FbApiPrivate
FbId lastmid;
gchar *contacts_delta;
int tweak;
+ gboolean is_work;
+ gboolean need_work_switch;
+ gchar *sso_verifier;
+ FbId work_community_id;
};
struct _FbApiData
@@ -72,6 +78,13 @@ struct _FbApiData
GDestroyNotify func;
};
+struct _FbApiPreloginData
+{
+ FbApi *api;
+ gchar *user;
+ gchar *pass;
+};
+
static void
fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg);
@@ -143,6 +156,9 @@ fb_api_set_property(GObject *obj, guint prop, const GValue *val,
priv->tweak = g_value_get_int(val);
fb_http_set_agent(priv->http, fb_api_get_agent_string(priv->tweak, 0));
break;
+ case PROP_WORK:
+ priv->is_work = g_value_get_boolean(val);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop, pspec);
@@ -177,6 +193,9 @@ fb_api_get_property(GObject *obj, guint prop, GValue *val, GParamSpec *pspec)
case PROP_TWEAK:
g_value_set_int(val, priv->tweak);
break;
+ case PROP_WORK:
+ g_value_set_boolean(val, priv->is_work);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop, pspec);
@@ -209,6 +228,7 @@ fb_api_dispose(GObject *obj)
g_free(priv->stoken);
g_free(priv->token);
g_free(priv->contacts_delta);
+ g_free(priv->sso_verifier);
}
static void
@@ -308,6 +328,16 @@ fb_api_class_init(FbApiClass *klass)
"",
0, G_MAXINT, 0,
G_PARAM_READWRITE);
+
+ /**
+ * FbApi:work:
+ */
+ props[PROP_WORK] = g_param_spec_boolean(
+ "work",
+ "Work",
+ "",
+ FALSE,
+ G_PARAM_READWRITE);
g_object_class_install_properties(gklass, PROP_N, props);
/**
@@ -546,6 +576,22 @@ fb_api_class_init(FbApiClass *klass)
fb_marshal_VOID__POINTER,
G_TYPE_NONE,
1, G_TYPE_POINTER);
+
+ /**
+ * FbApi::work-sso-login:
+ * @api: The #FbApi.
+ *
+ * Emitted when user interaction is required to continue SAML SSO login
+ */
+
+ g_signal_new("work-sso-login",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL,
+ fb_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
}
static void
@@ -764,7 +810,8 @@ fb_api_http_req(FbApi *api, const gchar *url, const gchar *name,
GList *l;
GString *gstr;
- fb_http_values_set_str(values, "api_key", FB_API_KEY);
+ fb_http_values_set_str(values, "api_key",
+ priv->is_work ? FB_WORK_API_KEY : FB_API_KEY);
fb_http_values_set_str(values, "device_id", priv->did);
fb_http_values_set_str(values, "fb_api_req_friendly_name", name);
fb_http_values_set_str(values, "format", "json");
@@ -787,7 +834,7 @@ fb_api_http_req(FbApi *api, const gchar *url, const gchar *name,
g_string_append_printf(gstr, "%s=%s", key, val);
}
- g_string_append(gstr, FB_API_SECRET);
+ g_string_append(gstr, priv->is_work ? FB_WORK_API_SECRET : FB_API_SECRET);
data = g_compute_checksum_for_string(G_CHECKSUM_MD5, gstr->str, gstr->len);
fb_http_values_set_str(values, "sig", data);
@@ -922,23 +969,26 @@ fb_api_cb_mqtt_open(FbMqtt *mqtt, gpointer data)
? fb_api_get_agent_string(priv->tweak, 1)
: FB_API_MQTT_AGENT);
- /* Write the UNKNOWN ("cp"?) */
+ /* Write the client capabilities */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 3, 2);
- fb_thrift_write_i64(thft, 23);
+ fb_thrift_write_i64(thft, FB_CP_ACKNOWLEDGED_DELIVERY |
+ FB_CP_PROCESSING_LASTACTIVE_PRESENCEINFO |
+ FB_CP_EXACT_KEEPALIVE |
+ FB_CP_DELTA_SENT_MESSAGE_ENABLED);
- /* Write the UNKNOWN ("ecp"?) */
+ /* Write the endpoint capabilitites */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 4, 3);
fb_thrift_write_i64(thft, 26);
- /* Write the UNKNOWN */
+ /* Write the publish payload format (deflate) */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 5, 4);
fb_thrift_write_i32(thft, 1);
- /* Write the UNKNOWN ("no_auto_fg"?) */
+ /* Write the noAutomaticForeground flag */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 6, 5);
fb_thrift_write_bool(thft, TRUE);
- /* Write the visibility state */
+ /* Write the visibility state (makeUserAvailableInForeground flag) */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 7, 6);
fb_thrift_write_bool(thft, !priv->invisible);
@@ -946,15 +996,15 @@ fb_api_cb_mqtt_open(FbMqtt *mqtt, gpointer data)
fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 8, 7);
fb_thrift_write_str(thft, priv->did);
- /* Write the UNKNOWN ("fg"?) */
+ /* Write the isInitiallyForeground flag */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 9, 8);
fb_thrift_write_bool(thft, TRUE);
- /* Write the UNKNOWN ("nwt"?) */
+ /* Write the network type (WIFI) */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 10, 9);
fb_thrift_write_i32(thft, 1);
- /* Write the UNKNOWN ("nwst"?) */
+ /* Write the network subtype (none) */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 11, 10);
fb_thrift_write_i32(thft, 0);
@@ -962,16 +1012,18 @@ fb_api_cb_mqtt_open(FbMqtt *mqtt, gpointer data)
fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 12, 11);
fb_thrift_write_i64(thft, priv->mid);
- /* Write the UNKNOWN */
+ /* Write the list of topics to subscribe */
fb_thrift_write_field(thft, FB_THRIFT_TYPE_LIST, 14, 12);
fb_thrift_write_list(thft, FB_THRIFT_TYPE_I32, 0);
+
+ /* Write the STOP for the struct */
fb_thrift_write_stop(thft);
/* Write the token */
- fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 15, 14);
+ fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 5, 4);
fb_thrift_write_str(thft, priv->token);
- /* Write the STOP for the struct */
+ /* Write the STOP for the message */
fb_thrift_write_stop(thft);
bytes = fb_thrift_get_bytes(thft);
@@ -2104,6 +2156,53 @@ fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg)
}
static void
+fb_api_cb_work_peek(FbHttpRequest *req, gpointer data)
+{
+ FbApi *api = data;
+ FbApiPrivate *priv = api->priv;
+ GError *err = NULL;
+ JsonNode *root;
+ gchar *community = NULL;
+
+ if (!fb_api_http_chk(api, req, &root)) {
+ return;
+ }
+
+ /* The work_users[0] explicitly only handles the first user.
+ * If more than one user is ever needed, this is what you want to change,
+ * but as far as I know this feature (linked work accounts) is deprecated
+ * and most users can detach their work accounts from their personal
+ * accounts by assigning a password to the work account. */
+ community = fb_json_node_get_str(root,
+ "$.data.viewer.work_users[0].community.login_identifier", &err);
+
+ FB_API_ERROR_EMIT(api, err,
+ g_free(community);
+ json_node_free(root);
+ return;
+ );
+
+ priv->work_community_id = FB_ID_FROM_STR(community);
+
+ fb_api_auth(api, "X", "X", "personal_to_work_switch");
+
+ g_free(community);
+ json_node_free(root);
+}
+
+static FbHttpRequest *
+fb_api_work_peek(FbApi *api)
+{
+ FbHttpValues *prms;
+
+ prms = fb_http_values_new();
+ fb_http_values_set_int(prms, "doc_id", FB_API_WORK_COMMUNITY_PEEK);
+
+ return fb_api_http_req(api, FB_API_URL_GQL, "WorkCommunityPeekQuery",
+ "post", prms, fb_api_cb_work_peek);
+}
+
+static void
fb_api_cb_auth(FbHttpRequest *req, gpointer data)
{
FbApi *api = data;
@@ -2118,7 +2217,14 @@ fb_api_cb_auth(FbHttpRequest *req, gpointer data)
values = fb_json_values_new(root);
fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.access_token");
- fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.uid");
+
+ /* extremely silly difference */
+ if (priv->is_work) {
+ fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.uid");
+ } else {
+ fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.uid");
+ }
+
fb_json_values_update(values, &err);
FB_API_ERROR_EMIT(api, err,
@@ -2129,25 +2235,202 @@ fb_api_cb_auth(FbHttpRequest *req, gpointer data)
g_free(priv->token);
priv->token = fb_json_values_next_str_dup(values, NULL);
- priv->uid = fb_json_values_next_int(values, 0);
- g_signal_emit_by_name(api, "auth");
+ if (priv->is_work) {
+ priv->uid = FB_ID_FROM_STR(fb_json_values_next_str(values, "0"));
+ } else {
+ priv->uid = fb_json_values_next_int(values, 0);
+ }
+
+ if (priv->need_work_switch) {
+ fb_api_work_peek(api);
+ priv->need_work_switch = FALSE;
+ } else {
+ g_signal_emit_by_name(api, "auth");
+ }
+
g_object_unref(values);
json_node_free(root);
}
void
-fb_api_auth(FbApi *api, const gchar *user, const gchar *pass)
+fb_api_auth(FbApi *api, const gchar *user, const gchar *pass, const gchar *credentials_type)
{
+ FbApiPrivate *priv = api->priv;
FbHttpValues *prms;
prms = fb_http_values_new();
fb_http_values_set_str(prms, "email", user);
fb_http_values_set_str(prms, "password", pass);
+
+ if (credentials_type) {
+ fb_http_values_set_str(prms, "credentials_type", credentials_type);
+ }
+
+ if (priv->sso_verifier) {
+ fb_http_values_set_str(prms, "code_verifier", priv->sso_verifier);
+ g_free(priv->sso_verifier);
+ priv->sso_verifier = NULL;
+ }
+
+ if (priv->work_community_id) {
+ fb_http_values_set_int(prms, "community_id", priv->work_community_id);
+ }
+
+ if (priv->is_work && priv->token) {
+ fb_http_values_set_str(prms, "access_token", priv->token);
+ }
+
fb_api_http_req(api, FB_API_URL_AUTH, "authenticate", "auth.login", prms,
fb_api_cb_auth);
}
+static void
+fb_api_cb_work_prelogin(FbHttpRequest *req, gpointer data)
+{
+ FbApiPreloginData *pata = data;
+ FbApi *api = pata->api;
+ FbApiPrivate *priv = api->priv;
+ GError *err = NULL;
+ JsonNode *root;
+ gchar *status;
+ gchar *user = pata->user;
+ gchar *pass = pata->pass;
+
+ g_free(pata);
+
+ if (!fb_api_http_chk(api, req, &root)) {
+ return;
+ }
+
+ status = fb_json_node_get_str(root, "$.status", &err);
+
+ FB_API_ERROR_EMIT(api, err,
+ json_node_free(root);
+ return;
+ );
+
+ if (g_strcmp0(status, "can_login_password") == 0) {
+ fb_api_auth(api, user, pass, "work_account_password");
+
+ } else if (g_strcmp0(status, "can_login_via_linked_account") == 0) {
+ fb_api_auth(api, user, pass, "personal_account_password_with_work_username");
+ priv->need_work_switch = TRUE;
+
+ } else if (g_strcmp0(status, "can_login_sso") == 0) {
+ g_signal_emit_by_name(api, "work-sso-login");
+
+ } else if (g_strcmp0(status, "cannot_login") == 0) {
+ char *reason = fb_json_node_get_str(root, "$.cannot_login_reason", NULL);
+
+ if (g_strcmp0(reason, "non_business_email") == 0) {
+ fb_api_error(api, FB_API_ERROR_AUTH,
+ "Cannot login with non-business email. "
+ "Change the 'username' setting or disable 'work'");
+ } else {
+ char *title = fb_json_node_get_str(root, "$.error_title", NULL);
+ char *body = fb_json_node_get_str(root, "$.error_body", NULL);
+
+ fb_api_error(api, FB_API_ERROR_AUTH,
+ "Work prelogin failed (%s - %s)", title, body);
+
+ g_free(title);
+ g_free(body);
+ }
+
+ g_free(reason);
+
+ } else if (g_strcmp0(status, "can_self_invite") == 0) {
+ fb_api_error(api, FB_API_ERROR_AUTH, "Unknown email. "
+ "Change the 'username' setting or disable 'work'");
+ }
+
+ g_free(status);
+ json_node_free(root);
+}
+
+void
+fb_api_work_login(FbApi *api, gchar *user, gchar *pass)
+{
+ FbApiPrivate *priv = api->priv;
+ FbHttpRequest *req;
+ FbHttpValues *prms, *hdrs;
+ FbApiPreloginData *pata = g_new0(FbApiPreloginData, 1);
+
+ pata->api = api;
+ pata->user = user;
+ pata->pass = pass;
+
+ priv->is_work = TRUE;
+
+ req = fb_http_request_new(priv->http, FB_API_URL_WORK_PRELOGIN, TRUE,
+ fb_api_cb_work_prelogin, pata);
+
+ hdrs = fb_http_request_get_headers(req);
+ fb_http_values_set_str(hdrs, "Authorization", "OAuth null");
+
+ prms = fb_http_request_get_params(req);
+ fb_http_values_set_str(prms, "email", user);
+ fb_http_values_set_str(prms, "access_token",
+ FB_WORK_API_KEY "|" FB_WORK_API_SECRET);
+
+ fb_http_request_send(req);
+}
+
+gchar *
+fb_api_work_gen_sso_url(FbApi *api, const gchar *user)
+{
+ FbApiPrivate *priv = api->priv;
+ gchar *challenge, *verifier, *req_id, *email;
+ gchar *ret;
+
+ fb_util_gen_sso_verifier(&challenge, &verifier, &req_id);
+
+ email = g_uri_escape_string(user, NULL, FALSE);
+
+ ret = g_strdup_printf(FB_API_SSO_URL, req_id, challenge, email);
+
+ g_free(req_id);
+ g_free(challenge);
+ g_free(email);
+
+ g_free(priv->sso_verifier);
+ priv->sso_verifier = verifier;
+
+ return ret;
+}
+
+void
+fb_api_work_got_nonce(FbApi *api, const gchar *url)
+{
+ gchar **split;
+ gchar *uid = NULL;
+ gchar *nonce = NULL;
+ int i;
+
+ if (!g_str_has_prefix(url, "fb-workchat-sso://sso/?")) {
+ return;
+ }
+
+ split = g_strsplit(strchr(url, '?'), "&", -1);
+
+ for (i = 0; split[i]; i++) {
+ gchar *eq = strchr(split[i], '=');
+
+ if (g_str_has_prefix(split[i], "uid=")) {
+ uid = g_strstrip(eq + 1);
+ } else if (g_str_has_prefix(split[i], "nonce=")) {
+ nonce = g_strstrip(eq + 1);
+ }
+ }
+
+ if (uid && nonce) {
+ fb_api_auth(api, uid, nonce, "work_sso_nonce");
+ }
+
+ g_strfreev(split);
+}
+
static gchar *
fb_api_user_icon_checksum(gchar *icon)
{
@@ -2252,6 +2535,8 @@ fb_api_cb_contacts_nodes(FbApi *api, JsonNode *root, GSList *users)
"$.represented_profile.id");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.represented_profile.friendship_status");
+ fb_json_values_add(values, FB_JSON_TYPE_BOOL, FALSE,
+ "$.is_on_viewer_contact_list");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.structured_name.text");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
@@ -2264,11 +2549,14 @@ fb_api_cb_contacts_nodes(FbApi *api, JsonNode *root, GSList *users)
}
while (fb_json_values_update(values, &err)) {
+ gboolean in_contact_list;
+
str = fb_json_values_next_str(values, "0");
uid = FB_ID_FROM_STR(str);
str = fb_json_values_next_str(values, NULL);
+ in_contact_list = fb_json_values_next_bool(values, FALSE);
- if (((g_strcmp0(str, "ARE_FRIENDS") != 0) &&
+ if ((!in_contact_list && (g_strcmp0(str, "ARE_FRIENDS") != 0) &&
(uid != priv->uid)) || (uid == 0))
{
if (!is_array) {
diff --git a/facebook/facebook-api.h b/facebook/facebook-api.h
index 3ed0e41..988dbf5 100644
--- a/facebook/facebook-api.h
+++ b/facebook/facebook-api.h
@@ -89,6 +89,20 @@
#define FB_API_SECRET "374e60f8b9bb6b8cbb30f78030438895"
/**
+ * FB_WORK_API_KEY:
+ *
+ * The Facebook workchat app API key.
+ */
+#define FB_WORK_API_KEY "312713275593566"
+
+/**
+ * FB_WORK_API_SECRET:
+ *
+ * The Facebook workchat app API secret.
+ */
+#define FB_WORK_API_SECRET "d2901dc6cb685df3b074b30b56b78d28"
+
+/**
* FB_ORCA_AGENT
*
* The part of the user agent that looks like the official client, since the
@@ -103,7 +117,7 @@
*
*/
-#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/38.0.0.22.155;FBBV/14477681]"
+#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/192.0.0.31.101;FBBV/14477681]"
/**
* FB_API_AGENT:
@@ -138,6 +152,15 @@
#define FB_API_URL_AUTH FB_API_BHOST "/method/auth.login"
/**
+ * FB_API_URL_WORK_PRELOGIN
+ *
+ * The URL for workchat pre-login information, indicating what auth method
+ * should be used
+ */
+
+#define FB_API_URL_WORK_PRELOGIN FB_API_GHOST "/at_work/pre_login_info"
+
+/**
* FB_API_URL_GQL:
*
* The URL for GraphQL requests.
@@ -173,6 +196,14 @@
#define FB_API_URL_TOPIC FB_API_AHOST "/method/messaging.setthreadname"
/**
+ * FB_API_SSO_URL:
+ *
+ * Template for the URL shown to workchat users when trying to authenticate
+ * with SSO.
+ */
+#define FB_API_SSO_URL "https://m.facebook.com/work/sso/mobile?app_id=312713275593566&response_url=fb-workchat-sso%%3A%%2F%%2Fsso&request_id=%s&code_challenge=%s&email=%s"
+
+/**
* FB_API_QUERY_CONTACT:
*
* The query hash for the `UsersQuery`.
@@ -320,6 +351,16 @@
#define FB_API_QUERY_XMA 10153919431161729
/**
+ * FB_API_WORK_COMMUNITY_PEEK:
+ *
+ * The docid with information about the work community of the currently
+ * authenticated user.
+ *
+ * Used when prelogin returns can_login_via_linked_account
+ */
+#define FB_API_WORK_COMMUNITY_PEEK 1295334753880530
+
+/**
* FB_API_CONTACTS_COUNT:
*
* The maximum amount of contacts to fetch in a single request. If this
@@ -447,6 +488,39 @@ typedef enum
} FbApiMessageFlags;
/**
+* FbApiClientCapabilities:
+* @FB_CP_ACKNOWLEDGED_DELIVERY:
+* @FB_CP_PROCESSING_LASTACTIVE_PRESENCEINFO:
+* @FB_CP_EXACT_KEEPALIVE:
+* @FB_CP_REQUIRES_JSON_UNICODE_ESCAPES:
+* @FB_CP_DELTA_SENT_MESSAGE_ENABLED:
+* @FB_CP_USE_ENUM_TOPIC: All topics are numeric.
+* @FB_CP_SUPPRESS_GETDIFF_IN_CONNECT:
+* @FB_CP_USE_THRIFT_FOR_INBOX:
+* @FB_CP_USE_SEND_PINGRESP:
+* @FB_CP_REQUIRE_REPLAY_PROTECTION:
+* @FB_CP_DATA_SAVING_MODE:
+* @FB_CP_TYPING_OFF_WHEN_SENDING_MESSAGE:
+*
+* The client capabilities.
+*/
+typedef enum
+{
+ FB_CP_ACKNOWLEDGED_DELIVERY = 1 << 0,
+ FB_CP_PROCESSING_LASTACTIVE_PRESENCEINFO = 1 << 1,
+ FB_CP_EXACT_KEEPALIVE = 1 << 2,
+ FB_CP_REQUIRES_JSON_UNICODE_ESCAPES = 1 << 3,
+ FB_CP_DELTA_SENT_MESSAGE_ENABLED = 1 << 4,
+ FB_CP_USE_ENUM_TOPIC = 1 << 5,
+ FB_CP_SUPPRESS_GETDIFF_IN_CONNECT = 1 << 6,
+ FB_CP_USE_THRIFT_FOR_INBOX = 1 << 7,
+ FB_CP_USE_SEND_PINGRESP = 1 << 8,
+ FB_CP_REQUIRE_REPLAY_PROTECTION = 1 << 9,
+ FB_CP_DATA_SAVING_MODE = 1 << 10,
+ FB_CP_TYPING_OFF_WHEN_SENDING_MESSAGE = 1 << 11
+} FbApiClientCapabilities;
+
+/**
* FbApi:
*
* Represents a Facebook Messenger connection.
@@ -641,12 +715,49 @@ fb_api_error_emit(FbApi *api, GError *error);
* @api: The #FbApi.
* @user: The Facebook user name, email, or phone number.
* @pass: The Facebook password.
+ * @credentials_type: Type of work account credentials, or NULL
*
* Sends an authentication request to Facebook. This will obtain
* session information, which is required for all other requests.
*/
void
-fb_api_auth(FbApi *api, const gchar *user, const gchar *pass);
+fb_api_auth(FbApi *api, const gchar *user, const gchar *pass, const gchar *credentials_type);
+
+/**
+ * fb_api_work_login:
+ * @api: The #FbApi.
+ * @user: The Facebook user name, email, or phone number.
+ * @pass: The Facebook password.
+ *
+ * Starts the workchat login sequence.
+ */
+void
+fb_api_work_login(FbApi *api, gchar *user, gchar *pass);
+
+/**
+ * fb_api_work_gen_sso_url:
+ * @api: The #FbApi.
+ * @user: The Facebook user email.
+ *
+ * Generates the URL to be shown to the user to get the SSO auth token. This
+ * url contains a challenge and the corresponding verifier is saved in the
+ * FbApi instance to be used later.
+ *
+ * Returns: a newly allocated string.
+ */
+gchar *
+fb_api_work_gen_sso_url(FbApi *api, const gchar *user);
+
+/**
+ * fb_api_work_got_nonce:
+ * @api: The #FbApi.
+ * @url: The fb-workchat-sso:// URL as entered by the user
+ *
+ * Parses the fb-workchat-sso:// URL that the user got redirected to and
+ * continues with work_sso_nonce auth
+ */
+void
+fb_api_work_got_nonce(FbApi *api, const gchar *url);
/**
* fb_api_contact:
diff --git a/facebook/facebook-data.c b/facebook/facebook-data.c
index 99cd5e5..608d725 100644
--- a/facebook/facebook-data.c
+++ b/facebook/facebook-data.c
@@ -169,6 +169,14 @@ fb_data_load(FbData *fata)
g_value_unset(&val);
}
+ num = set_getbool(&acct->set, "work");
+ if (num != 0) {
+ g_value_init(&val, G_TYPE_BOOLEAN);
+ g_value_set_boolean(&val, num);
+ g_object_set_property(G_OBJECT(priv->api), "work", &val);
+ g_value_unset(&val);
+ }
+
fb_api_rehash(priv->api);
return ret;
}
diff --git a/facebook/facebook-mqtt.c b/facebook/facebook-mqtt.c
index 9610bbd..01979e6 100644
--- a/facebook/facebook-mqtt.c
+++ b/facebook/facebook-mqtt.c
@@ -340,26 +340,33 @@ fb_mqtt_cb_read(gpointer data, gint fd, b_input_condition cond)
g_byte_array_set_size(priv->rbuf, 0);
res = ssl_read(priv->ssl, (gchar *) &byte, sizeof byte);
- g_byte_array_append(priv->rbuf, &byte, sizeof byte);
- if (res != sizeof byte) {
+ if (res < 0 && ssl_sockerr_again(priv->ssl)) {
+ return TRUE;
+ } else if (res != 1) {
fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL,
"Failed to read fixed header");
return FALSE;
}
+ g_byte_array_append(priv->rbuf, &byte, sizeof byte);
+
mult = 1;
do {
res = ssl_read(priv->ssl, (gchar *) &byte, sizeof byte);
- g_byte_array_append(priv->rbuf, &byte, sizeof byte);
- if (res != sizeof byte) {
+ /* TODO: this case isn't handled yet */
+ if (0 && res < 0 && ssl_sockerr_again(priv->ssl)) {
+ return TRUE;
+ } else if (res != 1) {
fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL,
"Failed to read packet size");
return FALSE;
}
+ g_byte_array_append(priv->rbuf, &byte, sizeof byte);
+
priv->remz += (byte & 127) * mult;
mult *= 128;
} while ((byte & 128) != 0);
@@ -369,7 +376,9 @@ fb_mqtt_cb_read(gpointer data, gint fd, b_input_condition cond)
size = MIN(priv->remz, sizeof buf);
rize = ssl_read(priv->ssl, (gchar *) buf, size);
- if (rize < 1) {
+ if (rize < 0 && ssl_sockerr_again(priv->ssl)) {
+ return TRUE;
+ } else if (rize < 1) {
fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL,
"Failed to read packet data");
return FALSE;
diff --git a/facebook/facebook-util.c b/facebook/facebook-util.c
index 15c4d4a..e101abe 100644
--- a/facebook/facebook-util.c
+++ b/facebook/facebook-util.c
@@ -376,3 +376,46 @@ fb_util_zlib_inflate(const GByteArray *bytes, GError **error)
g_object_unref(conv);
return ret;
}
+
+gchar *
+fb_util_urlsafe_base64_encode(const guchar *data, gsize len)
+{
+ gchar *out = g_base64_encode(data, len);
+ gchar *c;
+
+ for (c = out; *c; c++) {
+ if (*c == '+') {
+ *c = '-';
+ } else if (*c == '/') {
+ *c = '_';
+ } else if (*c == '=') {
+ *c = '\0';
+ break;
+ }
+ }
+
+ return out;
+}
+
+void
+fb_util_gen_sso_verifier(gchar **challenge, gchar **verifier, gchar **req_id)
+{
+ guint8 buf[32];
+ GChecksum *gc;
+ gsize digest_len = sizeof buf;
+
+ random_bytes(buf, sizeof buf);
+
+ *verifier = fb_util_urlsafe_base64_encode(buf, sizeof buf);
+
+ gc = g_checksum_new(G_CHECKSUM_SHA256);
+ g_checksum_update(gc, (guchar *) *verifier, -1);
+ g_checksum_get_digest(gc, buf, &digest_len);
+ g_checksum_free(gc);
+
+ *challenge = fb_util_urlsafe_base64_encode(buf, sizeof buf);
+
+ random_bytes(buf, 3);
+
+ *req_id = fb_util_urlsafe_base64_encode(buf, 3);
+}
diff --git a/facebook/facebook-util.h b/facebook/facebook-util.h
index a595cf3..b080eb4 100644
--- a/facebook/facebook-util.h
+++ b/facebook/facebook-util.h
@@ -289,4 +289,30 @@ fb_util_zlib_deflate(const GByteArray *bytes, GError **error);
GByteArray *
fb_util_zlib_inflate(const GByteArray *bytes, GError **error);
+/**
+ * fb_util_urlsafe_base64_encode:
+ * @data: the binary data to encode.
+ * @len: the length of data
+ *
+ * Wrapper around g_base64_encode() which substitutes '-' instead of '+'
+ * and '_' instead of '/' and removes the padding
+ *
+ * Returns: A newly allocated string.
+ */
+
+gchar *
+fb_util_urlsafe_base64_encode(const guchar *data, gsize len);
+
+/**
+ * fb_util_gen_sso_verifier:
+ * @challenge: base64 of sha256 of verifier
+ * @verifier: base64 of random data
+ * @req_id: base64 of random data
+ *
+ * Generates the challenge/response parameters used for the workchat SSO auth.
+ * All parameters are output parameters.
+ */
+void
+fb_util_gen_sso_verifier(gchar **challenge, gchar **verifier, gchar **req_id);
+
#endif /* _FACEBOOK_UTIL_H_ */
diff --git a/facebook/facebook.c b/facebook/facebook.c
index 526ccfe..0ced73f 100644
--- a/facebook/facebook.c
+++ b/facebook/facebook.c
@@ -26,6 +26,8 @@
#define OPT_SELFMESSAGE 0
#endif
+#define FB_SSO_HANDLE "facebook_sso_auth"
+
typedef enum {
FB_PTRBIT_NEW_BUDDY,
FB_PTRBIT_UNREAD_MSG
@@ -138,6 +140,9 @@ fb_cb_api_auth(FbApi *api, gpointer data)
ic = fb_data_get_connection(fata);
+ /* likely a no-op if not authing with SSO */
+ imcb_remove_buddy(ic, FB_SSO_HANDLE, NULL);
+
imcb_log(ic, "Fetching contacts");
fb_data_save(fata);
fb_api_contacts(api);
@@ -692,6 +697,31 @@ fb_cb_api_typing(FbApi *api, FbApiTyping *typg, gpointer data)
imcb_buddy_typing(ic, uid, flags);
}
+static void
+fb_cb_api_work_sso_login(FbApi *api, gpointer data)
+{
+ FbData *fata = data;
+ struct im_connection *ic;
+ gchar *url;
+
+ ic = fb_data_get_connection(fata);
+
+ url = fb_api_work_gen_sso_url(api, ic->acc->user);
+ imcb_add_buddy(ic, FB_SSO_HANDLE, NULL);
+
+ imcb_buddy_msg(ic, FB_SSO_HANDLE, "Open this URL in your browser to authenticate:", 0, 0);
+ imcb_buddy_msg(ic, FB_SSO_HANDLE, url, 0, 0);
+ imcb_buddy_msg(ic, FB_SSO_HANDLE,
+ "Respond to this message with the URL starting with 'fb-workchat-sso://' that it attempts to redirect to.",
+ 0, 0);
+ imcb_buddy_msg(ic, FB_SSO_HANDLE,
+ "If your browser says 'Address not understood' (like firefox), copy it from the address bar. "
+ "Otherwise you might have to right click -> view source in the last page and find it there. Good luck!",
+ 0, 0);
+
+ g_free(url);
+}
+
static char *
fb_eval_open(struct set *set, char *value)
{
@@ -743,6 +773,7 @@ fb_init(account_t *acct)
set_add(&acct->set, "mark_read_reply", "false", set_eval_bool, acct);
set_add(&acct->set, "show_unread", "false", set_eval_bool, acct);
set_add(&acct->set, "sync_interval", "5", set_eval_int, acct);
+ set_add(&acct->set, "work", "false", set_eval_bool, acct);
}
static void
@@ -813,10 +844,18 @@ fb_login(account_t *acc)
"typing",
G_CALLBACK(fb_cb_api_typing),
fata);
+ g_signal_connect(api,
+ "work-sso-login",
+ G_CALLBACK(fb_cb_api_work_sso_login),
+ fata);
if (!fb_data_load(fata)) {
imcb_log(ic, "Authenticating");
- fb_api_auth(api, acc->user, acc->pass);
+ if (set_getbool(&acc->set, "work")) {
+ fb_api_work_login(api, acc->user, acc->pass);
+ } else {
+ fb_api_auth(api, acc->user, acc->pass, NULL);
+ }
return;
}
@@ -848,6 +887,12 @@ fb_buddy_msg(struct im_connection *ic, char *to, char *message, int flags)
FbId uid;
api = fb_data_get_api(fata);
+
+ if (g_strcmp0(to, FB_SSO_HANDLE) == 0 && !(ic->flags & OPT_LOGGED_IN)) {
+ fb_api_work_got_nonce(api, message);
+ return 0;
+ }
+
uid = FB_ID_FROM_STR(to);
bu = bee_user_by_handle(ic->bee, ic, to);