Skip to content
This repository was archived by the owner on Mar 17, 2025. It is now read-only.

Refactored Firebase library #42

Merged
merged 19 commits into from
Jan 29, 2016
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 106 additions & 73 deletions Firebase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,114 +15,147 @@
//
#include "Firebase.h"

const char* firebaseFingerprint = "7A 54 06 9B DC 7A 25 B3 86 8D 66 53 48 2C 0B 96 42 C7 B3 0A";
const uint16_t firebasePort = 443;
namespace {
const char* kFirebaseFingerprint = "7A 54 06 9B DC 7A 25 B3 86 8D 66 53 48 2C 0B 96 42 C7 B3 0A";
const uint16_t kFirebasePort = 443;

Firebase::Firebase(const String& host) : _host(host) {
_http.setReuse(true);
String makeFirebaseURL(const String& path, const String& auth) {
String url;
if (path[0] != '/') {
url = "/";
}
url += path + ".json";
if (auth.length() > 0) {
url += "?auth=" + auth;
}
return url;
}

} // namespace

Firebase::Firebase(const String& host) : host_(host) {
http_.setReuse(true);
}

Firebase& Firebase::auth(const String& auth) {
_auth = auth;
auth_ = auth;
return *this;
}

String Firebase::get(const String& path) {
return sendRequestGetBody("GET", path);
FirebaseGet Firebase::get(const String& path) {
return FirebaseGet(host_, auth_, path, &http_);
}

FirebasePush Firebase::push(const String& path, const String& value) {
return FirebasePush(host_, auth_, path, value, &http_);
}

String Firebase::push(const String& path, const String& value) {
return sendRequestGetBody("POST", path, value);
FirebaseRemove Firebase::remove(const String& path) {
return FirebaseRemove(host_, auth_, path, &http_);
}

bool Firebase::remove(const String& path) {
int status = sendRequest("DELETE", path);
return status == HTTP_CODE_OK;
FirebaseStream Firebase::stream(const String& path) {
return FirebaseStream(host_, auth_, path); // stream doesn't reuse http client.
}

Firebase& Firebase::stream(const String& path) {
_error.reset();
String url = makeURL(path);
const char* headers[] = {"Location"};
_http.setReuse(true);
_http.begin(_host.c_str(), firebasePort, url.c_str(), true, firebaseFingerprint);
_http.collectHeaders(headers, 1);
_http.addHeader("Accept", "text/event-stream");
int statusCode = _http.sendRequest("GET", (uint8_t*)NULL, 0);
String location;
// TODO(proppy): Add a max redirect check
while (statusCode == 307) {
location = _http.header("Location");
_http.setReuse(false);
_http.end();
_http.setReuse(true);
_http.begin(location, firebaseFingerprint);
statusCode = _http.sendRequest("GET", (uint8_t*)NULL, 0);
// FirebaseCall
FirebaseCall::FirebaseCall(const String& host, const String& auth,
const char* method, const String& path,
const String& data, HTTPClient* http) {
if (!http) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably reuse the client for now (even for stream) and do that in a separate PR (that might be the cause of the crash we see with the stream sample)

http = &http_;
}
if (statusCode != 200) {
_error.set(statusCode,
"stream " + location + ": "
+ HTTPClient::errorToString(statusCode));

String url = makeFirebaseURL(path, auth);
http->setReuse(true);
http->begin(host, kFirebasePort, url, true, kFirebaseFingerprint);

bool followRedirect = false;
if (method == "STREAM") {
method = "GET";
http->addHeader("Accept", "text/event-stream");
followRedirect = true;
}
return *this;
}

String Firebase::makeURL(const String& path) {
String url;
if (path[0] != '/') {
url = "/";
if (followRedirect) {
const char* headers[] = {"Location"};
http->collectHeaders(headers, 1);
}
url += path + ".json";
if (_auth.length() > 0) {
url += "?auth=" + _auth;

int status = http->sendRequest(method, (uint8_t*)data.c_str(), data.length());

// TODO: Add a max redirect check
if (followRedirect) {
while (status == HTTP_CODE_TEMPORARY_REDIRECT) {
String location = http->header("Location");
http->setReuse(false);
http->end();
http->setReuse(true);
http->begin(location, kFirebaseFingerprint);
status = http->sendRequest(method, (uint8_t*)NULL, 0);
}
}

if (status != 200) {
error_ = FirebaseError(status, String(method) + " " + url + ": " + HTTPClient::errorToString(status));
}
return url;
}

int Firebase::sendRequest(const char* method, const String& path, const String& value) {
String url = makeURL(path);
_http.begin(_host.c_str(), firebasePort, url.c_str(), true, firebaseFingerprint);
int statusCode = _http.sendRequest(method, (uint8_t*)value.c_str(), value.length());
setError(method, url, statusCode);
return statusCode;
// if not streaming.
if (!followRedirect) {
response_ = http->getString();
}
}

String Firebase::sendRequestGetBody(const char* method, const String& path, const String& value) {
sendRequest(method, path, value);
if (_error.code() != 0) {
return "";
// FirebaseGet
FirebaseGet::FirebaseGet(const String& host, const String& auth,
const String& path,
HTTPClient* http)
: FirebaseCall(host, auth, "GET", path, "", http) {

if (!error()) {
// TODO: parse json
json_ = response();
}
// no _http.end() because of connection reuse.
return _http.getString();
}

void Firebase::setError(const char* method, const String& url, int statusCode) {
_error.reset();
if (statusCode < 0) {
_error.set(statusCode,
String(method) + " " + url + ": "
+ HTTPClient::errorToString(statusCode));
// FirebasePush
FirebasePush::FirebasePush(const String& host, const String& auth,
const String& path, const String& value,
HTTPClient* http)
: FirebaseCall(host, auth, "POST", path, value, http) {
if (!error()) {
// TODO: parse name
name_ = response();
}
}

bool Firebase::connected() {
return _http.connected();
// FirebasePush
FirebaseRemove::FirebaseRemove(const String& host, const String& auth,
const String& path,
HTTPClient* http)
: FirebaseCall(host, auth, "DELETE", path, "", http) {
}

// FirebaseStream
FirebaseStream::FirebaseStream(const String& host, const String& auth,
const String& path)
: FirebaseCall(host, auth, "STREAM", path) {
}

bool Firebase::available() {
return _http.getStreamPtr()->available();
bool FirebaseStream::available() {
return http_.getStreamPtr()->available();
}

Firebase::Event Firebase::read(String& event) {
auto client = _http.getStreamPtr();
Event type;;
FirebaseStream::Event FirebaseStream::read(String& event) {
auto client = http_.getStreamPtr();
Event type;
String typeStr = client->readStringUntil('\n').substring(7);
if (typeStr == "put") {
type = Firebase::Event::PUT;
type = Event::PUT;
} else if (typeStr == "patch") {
type = Firebase::Event::PATCH;
type = Event::PATCH;
} else {
type = Firebase::Event::UNKNOWN;
type = Event::UNKNOWN;
}
event = client->readStringUntil('\n').substring(6);
client->readStringUntil('\n'); // consume separator
Expand Down
133 changes: 102 additions & 31 deletions Firebase.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,52 +25,123 @@
#include <WiFiClientSecure.h>
#include <ESP8266HTTPClient.h>

// FirebaseError represents a Firebase API error with a code and a
// message.
class FirebaseGet;
class FirebasePush;
class FirebaseRemove;
class FirebaseStream;

// Primary client to the Firebase backend.
class Firebase {
public:
Firebase(const String& host);
Firebase& auth(const String& auth);

// Fetch result at "path".
FirebaseGet get(const String& path);

// Add new value to list at "path", will return key for the new item.
FirebasePush push(const String& path, const String& value);

// Deletes value at "path" from firebase.
FirebaseRemove remove(const String& path);

// Starts a stream of events that affect object at "path".
FirebaseStream stream(const String& path);

private:
HTTPClient http_;
String host_;
String auth_;
};

class FirebaseError {
public:
operator bool() const { return _code < 0; }
int code() const { return _code; }
const String& message() const { return _message; }
void reset() { set(0, ""); }
void set(int code, const String& message) {
_code = code;
_message = message;
FirebaseError() {}
FirebaseError(int code, const String& message) : code_(code), message_(message) {
}
operator bool() const { return code_ != 0; }
int code() const { return code_; }
const String& message() const { return message_; }
private:
int code_ = 0;
String message_ = "";
};

class FirebaseCall {
public:
FirebaseCall(const String& host, const String& auth,
const char* method, const String& path,
const String& data = "",
HTTPClient* http = NULL);
const FirebaseError& error() const {
return error_;
}
const String& response() {
return response_;
}
protected:
HTTPClient http_;
FirebaseError error_;
String response_;
};

class FirebaseGet : public FirebaseCall {
public:
FirebaseGet(const String& host, const String& auth,
const String& path, HTTPClient* http = NULL);

const String& json() const {
return json_;
}

private:
int _code = 0;
String _message = "";
String json_;
};

// Firebase is the connection to firebase.
class Firebase {
class FirebasePush : public FirebaseCall {
public:
Firebase(const String& host);
Firebase& auth(const String& auth);
const FirebaseError& error() const {
return _error;
FirebasePush(const String& host, const String& auth,
const String& path, const String& value, HTTPClient* http = NULL);

const String& name() const {
return name_;
}
String get(const String& path);
String push(const String& path, const String& value);
bool remove(const String& path);
bool connected();
Firebase& stream(const String& path);

private:
String name_;
};

class FirebaseRemove : public FirebaseCall {
public:
FirebaseRemove(const String& host, const String& auth,
const String& path, HTTPClient* http = NULL);
};


class FirebaseStream : public FirebaseCall {
public:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably allow a lazy construction of the FirebaseStream object (and even the other type) through a begin() method, that seems to be a popular pattern in the Arduino world, and would solve some of the lifecycle headache.

FirebaseStream(const String& host, const String& auth,
const String &path);

// True if there is an event available.
bool available();

// event type.
enum Event {
UNKNOWN,
PUT,
PATCH
};
Event read(String& event);

// Read next event in stream.
Event read(String& event);

const FirebaseError& error() const {
return _error;
}

private:
String makeURL(const String& path);
int sendRequest(const char* method, const String& path, const String& value = "");
String sendRequestGetBody(const char* method, const String& path, const String& value = "");
void setError(const char* method, const String& url, int status_code);

HTTPClient _http;
String _host;
String _auth;
HTTPClient http_;
FirebaseError _error;
};

Expand Down
Loading