diff --git a/CalDAV/Main.cpp b/CalDAV/Main.cpp --- a/CalDAV/Main.cpp +++ b/CalDAV/Main.cpp @@ -1,620 +1,620 @@ // https://developers.google.com/calendar/v3/reference/ #include "lgi/common/Lgi.h" #include "resdefs.h" #include "lgi/common/TextLog.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/Base64.h" #include "lgi/common/NetTools.h" #include "lgi/common/Json.h" #include "lgi/common/OptionsFile.h" ////////////////////////////////////////////////////////////////// const char *AppName = "CalDAV"; enum Ctrls { M_LOG = 100, }; #define LOCALHOST_PORT 54900 const char *ClientID = "968484784648-2igrbn8trpv01vlia1063ms7kht13i6q.apps.googleusercontent.com"; const char *ClientSecret = "rrziUs4iEmJIa4bbXaNk67zM"; const char *ApiKey = "AIzaSyA1y-lV27DY4RFa0YHgNf5skNp6BkNxs4o"; const char *Api = "https://apidata.googleusercontent.com/caldav/v2/%s/events/"; const char *CalID = "memecode%40gmail.com"; // const char *CalID = "ct3g3gt7novjg03pqtojhaapqc%40group.calendar.google.com"; const char *Scope = "https://www.googleapis.com/auth/calendar"; const char *AuthServer = "https://accounts.google.com/o/oauth2/v2/auth"; LString Read(LSocketI *s) { char Buf[256]; auto Rd = s->Read(Buf, sizeof(Buf)); return Rd > 0 ? LString(Buf, Rd) : NULL; } bool Write(LSocketI *s, LString b) { for (size_t i = 0; i < b.Length(); ) { auto Wr = s->Write(b.Get() + i, b.Length() - i); if (Wr <= 0) return false; i += Wr; } return true; } LString GetHeaders(LSocketI *s) { LString p; uint64 Start = LCurrentTime(); while (LCurrentTime() - Start < 4000) { if (!s->IsOpen()) { return NULL; } if (s->IsReadable(100)) { p += Read(s); if (p.Find("\r\n\r\n") >= 0) return p; } } s->Close(); return NULL; } bool GetHttp(LSocketI *s, LString &Hdrs, LString &Body, bool IsResponse, LStream *Log = NULL) { LString Resp = GetHeaders(s); char Buf[256]; ssize_t Rd; auto BodyPos = Resp.Find("\r\n\r\n"); LAutoString Len(InetGetHeaderField(Resp, "Content-Length", BodyPos)); if (Len) { int Bytes = atoi(Len); size_t Total = BodyPos + 4 + Bytes; while (Resp.Length() < Total) { Rd = s->Read(Buf, sizeof(Buf)); if (Rd > 0) { Resp += LString(Buf, Rd); } } Body = Resp(BodyPos + 4, -1); } else if (s->IsOpen() && IsResponse) { LAutoString Te(InetGetHeaderField(Resp, "Transfer-Encoding", BodyPos)); bool Chunked = Te ? !stricmp(Te, "chunked") : false; if (Chunked) { ssize_t Start = BodyPos + 4, Len = 0; Body.Empty(); do { ssize_t Eol; while ((Eol = Resp.Find("\r\n", Start)) < 0) Resp += Read(s); Len = htoi(Resp.Get() + Start); if (Len < 0) return false; Eol += 2; size_t Total = Eol + Len; while (Resp.Length() < Total) Resp += Read(s); if (Len > 0) Body += Resp(Eol, Total); Start = Total; } while (Len > 0); } else { auto Start = LCurrentTime(); while (true) { if (s->IsReadable(100)) { if ((Rd = s->Read(Buf, sizeof(Buf))) > 0) Resp += LString(Buf, Rd); else break; } else if (LCurrentTime() - Start >= 5000) { if (Log) Log->Print("Read timeout.\n"); break; } } Body = Resp(BodyPos + 4, -1); } } Hdrs = Resp(0, BodyPos); return true; } LString UrlFromHeaders(LString Hdrs) { auto Lines = Hdrs.Split("\r\n", 1); auto p = Lines[0].SplitDelimit(); if (p.Length() < 3) { return NULL; } return p[1]; } LString FormEncode(const char *s, bool InValue = true) { LStringPipe p; for (auto c = s; *c; c++) { if (isalpha(*c) || isdigit(*c) || *c == '_' || *c == '.' || (!InValue && *c == '+') || *c == '-' || *c == '%') { p.Write(c, 1); } else if (*c == ' ') { p.Write((char*)"+", 1); } else { p.Print("%%%02.2X", *c); } } - return p.NewGStr(); + return p.NewLStr(); } class LCalDAV : public LThread, public LCancel { LOptionsFile &Opts; LString Id; LStream *Log; LString Token, Access, Refresh; LString CodeVerifier; struct Server : public LSocket { LSocket Listen; LCalDAV *d; LSocket s; public: LHashTbl,LString> Params; LString Body; Server(LCalDAV *cd) : d(cd) { while (!Listen.Listen(LOCALHOST_PORT)) { if (d->IsCancelled()) break; d->Log->Print("Error: Can't listen on %i...\n", LOCALHOST_PORT); LSleep(1000); } } bool GetReq() { while (!d->IsCancelled()) { if (Listen.IsReadable(100)) { if (Listen.Accept(&s)) { // Read access code out of response LString Hdrs; if (GetHttp(&s, Hdrs, Body, false, d->Log)) { auto Url = UrlFromHeaders(Hdrs); auto Vars = Url.Split("?", 1); if (Vars.Length() != 2) { return false; } Vars = Vars[1].Split("&"); for (auto v : Vars) { auto p = v.Split("=", 1); if (p.Length() != 2) continue; Params.Add(p[0], p[1]); } return true; } } } } return false; } bool Response(const char *Txt) { LString Msg; Msg.Printf("HTTP/1.0 200 OK\r\n" "\r\n" "\n" "%s\n" "", Txt); return ::Write(&s, Msg); } }; LString Base64(LString s) { LString b; b.Length(BufferLen_BinTo64(s.Length())); auto ch = ConvertBinaryToBase64(b.Get(), b.Length(), (uchar*)s.Get(), s.Length()); return b; } LString ToText(LString Bin) { if (!Bin) return NULL; LString Txt; LArray t; for (char i='0'; i<='9'; i++) t.Add(i); for (char i='a'; i<='z'; i++) t.Add(i); for (char i='A'; i<='Z'; i++) t.Add(i); t.Add('-'); t.Add('.'); t.Add('_'); t.Add('~'); Txt.Length(Bin.Length()); int Pos = 0; for (int i=0; iPrint("Error: No CodeVerifier.\n"); return false; } LUri u(Endpoint); LString Uri, Redir, RedirEnc; Redir.Printf("http://localhost:%i", LOCALHOST_PORT); RedirEnc = u.EncodeStr(Redir, ":/"); Uri.Printf("%s?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&code_challenge=%s", AuthServer, ClientID, RedirEnc.Get(), Scope, CodeVerifier.Get()); LExecute(Uri); // Open browser for user to auth if (Svr.GetReq()) { Token = Svr.Params.Find("code"); Svr.Response(Token ? "Ok: Got token." : "Error: No token."); } return Token != NULL; } bool GetRefresh(const char *Ref) { Refresh = Ref; CodeVerifier = ToText(SslSocket::Random(48)); if (!CodeVerifier) return false; LString Body, Http; Body.Printf("client_id=%s&" "client_secret=%s&" "refresh_token=%s&" "grant_type=refresh_token&" "code_challenge=%s", FormEncode(ClientID).Get(), FormEncode(ClientSecret).Get(), FormEncode(Refresh).Get(), CodeVerifier.Get()); Http.Printf("POST /oauth2/v4/token HTTP/1.0\r\n" "Host: www.googleapis.com\r\n" "Content-Length: " LPrintfSizeT "\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "\r\n" "%s", Body.Length(), Body.Get()); LUri u(Scope); SslSocket sock(NULL, NULL, true); if (!sock.Open(u.sHost, HTTPS_PORT)) { Log->Print("Error: Can't connect to '%s:%i'\n", u.sHost.Get(), HTTPS_PORT); return NULL; } Log->Print("Refreshing access..\n"); if (!Write(&sock, Http)) return false; LString Hdrs; if (!GetHttp(&sock, Hdrs, Body, true, Log)) { Log->Print("%s:%i - No response.\n", _FL); return false; } // Log->Print("Body=%s\n", Body.Get()); LJson j(Body); Access = j.Get("access_token"); return Access != NULL; } bool GetAccess() { if (!CodeVerifier) { Log->Print("%s:%i - Error: no code verifier.\n", _FL); return false; } if (!Token) { Log->Print("%s:%i - Error: no token.\n", _FL); return false; } LStringPipe p(1024); LUri u(Scope); SslSocket sock(NULL, NULL, true); if (!sock.Open(u.sHost, HTTPS_PORT)) { Log->Print("Error: Can't connect to '%s:%i'\n", u.sHost.Get(), HTTPS_PORT); return NULL; } LString Body, Http; Body.Printf("code=%s&" "client_id=%s&" "client_secret=%s&" "redirect_uri=http://localhost:%i&" "code_verifier=%s&" "grant_type=authorization_code", FormEncode(Token).Get(), ClientID, ClientSecret, LOCALHOST_PORT, FormEncode(CodeVerifier).Get()); Http.Printf("POST /oauth2/v4/token HTTP/1.1\r\n" "Host: www.googleapis.com\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "Content-length: " LPrintfSizeT "\r\n" "\r\n" "%s", Body.Length(), Body.Get()); if (!Write(&sock, Http)) { Log->Print("%s:%i - Error writing to socket.\n", _FL); return false; } LString Hdrs; if (!GetHttp(&sock, Hdrs, Body, true, Log)) return false; LJson j(Body); Access = j.Get("access_token"); Refresh = j.Get("refresh_token"); return Access != NULL; } LString GetBody(LString Url) { LUri u(Url); LString Http; Http.Printf("GET %s HTTP/1.1\r\n" "Authorization: Bearer %s\r\n" "Host: %s\r\n" "\r\n", u.sPath.Get(), Access.Get(), u.sHost.Get()); SslSocket sock(NULL, NULL, true); if (!sock.Open(u.sHost, HTTPS_PORT)) { Log->Print("Error: Can't connect to '%s:%i'\n", u.sHost.Get(), HTTPS_PORT); return NULL; } #if 0 Log->Print("Connected to %s:%i\n%s\n\n", u.Host, HTTPS_PORT, Http.Get()); #endif if (!Write(&sock, Http)) { Log->Print("%s:%i - Write failed.\n", _FL); return false; } LString Hdrs, Body; if (!GetHttp(&sock, Hdrs, Body, true)) { Log->Print("%s:%i - Read failed.\n", _FL); return NULL; } return Body; } LString ListCalendars() { LString Path; Path.Printf("https://content.googleapis.com/calendar/v3/users/me/calendarList?key=%s", /*CalID,*/ ApiKey); return GetBody(Path); } LString GetEvents(const char *CalId) { LString Path; Path.Printf("https://content.googleapis.com/calendar/v3/calendars/%s/events?key=%s", CalId, ApiKey); return GetBody(Path); } public: LCalDAV(LStream *log, const char *id, LOptionsFile &opts) : Opts(opts), LThread("LCalDAV") { Log = log; Id = id; Run(); } ~LCalDAV() { Cancel(true); while (!IsExited()) LSleep(1); } int Main() { LVariant Ref; if (Opts.GetValue("refresh", Ref)) GetRefresh(Ref.Str()); if (!Access) { if (!GetToken()) { Log->Print("No token.\n"); return -1; } Log->Print("Got token.\n"); } if (!Access && !GetAccess()) { Log->Print("No access.\n"); return -1; } Log->Print("Got access.\n"); if (Refresh) Opts.SetValue("refresh", Ref = Refresh.Get()); LString Cals = ListCalendars(); if (Cals) { LJson j(Cals); Log->Print("\nCalendars:\n"); for (auto i : j.GetArray("items")) { auto id = i.Get("id"); auto summary = i.Get("summary"); Log->Print("\t%s = %s\n", summary.Get(), id.Get()); } // Log->Print("%s\n", Cals.Get()); } LString Events = GetEvents("memecode@gmail.com"); if (Events) { #if 1 LJson j(Events); Log->Print("\nEvents:\n"); auto Items = j.GetArray("items"); for (auto i : Items) { auto id = i.Get("id"); auto summary = i.Get("summary"); Log->Print("\t%s = %s\n", summary.Get(), id.Get()); } #else Log->Print("\n\n%s\n", Events.Get()); #endif } return 0; } }; class App : public LWindow { LOptionsFile Opts; LTextLog *Log; LAutoPtr Worker; public: App() : Log(NULL), Opts(LOptionsFile::PortableMode, AppName) { Name(AppName); LRect r(0, 0, 1000, 800); SetPos(r); MoveToCenter(); SetQuitOnClose(true); Opts.SerializeFile(false); LString ErrorMsg; auto Ssl = StartSSL(ErrorMsg, NULL); if (Attach(0)) { Log = new LTextLog(M_LOG); Log->Attach(this); Log->Focus(true); if (Ssl) Worker.Reset(new LCalDAV(Log, CalID, Opts)); else Log->Print("Error: No OpenSSL: %s\n", ErrorMsg.Get()); Visible(true); } } ~App() { EndSSL(); Opts.SerializeFile(true); } }; ////////////////////////////////////////////////////////////////// int LgiMain(OsAppArguments &AppArgs) { LApp a(AppArgs, AppName); if (a.IsOk()) { a.AppWnd = new App; a.Run(); } return 0; }