public void RefreshBookmarkMenu() { var mnuBookMarks = mMainWindow.mnuBookMarks; RoutedEventHandler clickHandler = mMainWindow.mnuMenuBookmarksGo_Click; mnuBookMarks.Items.Clear(); foreach (var line in BookmarkLines()) { var bmMenu = new MenuItem(); var linkParts = ParseGeminiLink(line); if (UriTester.TextIsUri(linkParts[0])) { bmMenu.CommandParameter = linkParts[0]; bmMenu.Header = linkParts[1]; bmMenu.ToolTip = linkParts[0]; bmMenu.Click += clickHandler; mnuBookMarks.Items.Add(bmMenu); } else if (line.Substring(0, 2) == "--") { mnuBookMarks.Items.Add(new Separator()); } else if (line.Substring(0, 2) == "__") { mnuBookMarks.Items.Add(new Separator()); } { //anything else in the bookmarks file ignored for now } } }
private void GoToPage_Executed(object sender, ExecutedRoutedEventArgs e) { if (UriTester.TextIsUri(txtUrl.Text)) { BrowserControl.Navigate(txtUrl.Text); } else { ToastNotify("Not a valid URI: " + txtUrl.Text, ToastMessageStyles.Error); } }
private void txtUrl_KeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { if (UriTester.TextIsUri(txtUrl.Text)) { BrowserControl.Navigate(txtUrl.Text); } else { ToastNotify("Not a valid URI: " + txtUrl.Text, ToastMessageStyles.Error); } } }
public void NavigateGeminiScheme(string fullQuery, System.Windows.Navigation.NavigatingCancelEventArgs e, SiteIdentity siteIdentity, bool requireSecure = true) { var geminiUri = e.Uri.OriginalString; var sessionPath = Session.Instance.SessionPath; var appDir = AppDomain.CurrentDomain.BaseDirectory; var settings = new UserSettings(); var hash = HashService.GetMd5Hash(fullQuery); //uses .txt as extension so content loaded as text/plain not interpreted by the browser //if user requests a view-source. var rawFile = sessionPath + "\\" + hash + ".txt"; var gmiFile = sessionPath + "\\" + hash + ".gmi"; var htmlFile = sessionPath + "\\" + hash + ".htm"; //delete txt file as GemGet seems to sometimes overwrite not create afresh File.Delete(rawFile); File.Delete(gmiFile); //delete any existing html file to encourage webbrowser to reload it File.Delete(htmlFile); var uri = new Uri(fullQuery); //use a proxy for any other scheme that is not gemini var proxy = ""; //use none var connectInsecure = false; X509Certificate2 certificate; certificate = Session.Instance.CertificatesManager.GetCertificate(uri.Host); //may be null if none assigned or available if (uri.Scheme != "gemini") { proxy = settings.HttpSchemeProxy; } try { GeminiResponse geminiResponse; try { geminiResponse = (GeminiResponse)Gemini.Fetch(uri, certificate, proxy, connectInsecure, settings.MaxDownloadSizeMb * 1024, settings.MaxDownloadTimeSeconds); } catch (Exception err) { //warn, but continue if there are server validation errors //in these early days of Gemini we dont forbid visiting a site with an expired cert or mismatched host name //but we do give a warning each time if (err.Message == "The remote certificate was rejected by the provided RemoteCertificateValidationCallback.") { mMainWindow.ToastNotify("Note: the certificate from: " + e.Uri.Authority + " is expired or invalid.", ToastMessageStyles.Warning); //try again insecure this time geminiResponse = (GeminiResponse)Gemini.Fetch(uri, certificate, proxy, true, settings.MaxDownloadSizeMb * 1024, settings.MaxDownloadTimeSeconds); } else { //reraise throw; } } if (geminiResponse.codeMajor == '1') { //needs input, then refetch mMainWindow.ToggleContainerControlsForBrowser(true); NavigateGeminiWithInput(e, geminiResponse); } else if (geminiResponse.codeMajor == '2') { //success File.WriteAllBytes(rawFile, geminiResponse.bytes.ToArray()); if (File.Exists(rawFile)) { if (geminiResponse.meta.Contains("text/gemini")) { File.Copy(rawFile, gmiFile); } else if (geminiResponse.meta.Contains("text/html")) { //is an html file served over gemini - probably not common, but not unheard of var htmltoGmiResult = ConverterService.HtmlToGmi(rawFile, gmiFile); if (htmltoGmiResult.Item1 != 0) { mMainWindow.ToastNotify("Could not convert HTML to GMI: " + fullQuery, ToastMessageStyles.Error); mMainWindow.ToggleContainerControlsForBrowser(true); e.Cancel = true; return; } } else if (geminiResponse.meta.Contains("text/")) { //convert plain text to a gemini version (wraps it in a preformatted section) var textToGmiResult = ConverterService.TextToGmi(rawFile, gmiFile); if (textToGmiResult.Item1 != 0) { mMainWindow.ToastNotify("Could not render text as GMI: " + fullQuery, ToastMessageStyles.Error); mMainWindow.ToggleContainerControlsForBrowser(true); e.Cancel = true; return; } } else { //a download //its an image - rename the raw file and just show it var pathFragment = (new UriBuilder(fullQuery)).Path; var ext = Path.GetExtension(pathFragment); var binFile = rawFile + (ext == "" ? ".tmp" : ext); File.Copy(rawFile, binFile, true); //rename overwriting if (geminiResponse.meta.Contains("image/")) { mMainWindow.ShowImage(fullQuery, binFile, e); } else { SaveFileDialog saveFileDialog = new SaveFileDialog(); saveFileDialog.FileName = Path.GetFileName(pathFragment); if (saveFileDialog.ShowDialog() == true) { try { //save the file var savePath = saveFileDialog.FileName; File.Copy(binFile, savePath, true); //rename overwriting mMainWindow.ToastNotify("File saved to " + savePath, ToastMessageStyles.Success); } catch (SystemException err) { mMainWindow.ToastNotify("Could not save the file due to: " + err.Message, ToastMessageStyles.Error); } } mMainWindow.ToggleContainerControlsForBrowser(true); e.Cancel = true; } return; } if (geminiResponse.uri.ToString() != fullQuery) { string redirectUri = fullQuery; if (geminiResponse.uri.ToString().Contains("://")) { //a full url //normalise the URi (e.g. remove default port if specified) redirectUri = UriTester.NormaliseUri(new Uri(geminiResponse.uri.ToString())).ToString(); } else { //a relative one var baseUri = new Uri(fullQuery); var targetUri = new Uri(baseUri, geminiResponse.uri.ToString()); redirectUri = UriTester.NormaliseUri(targetUri).ToString(); } var finalUri = new Uri(redirectUri); if (e.Uri.Scheme == "gemini" && finalUri.Scheme != "gemini") { //cross-scheme redirect, not supported mMainWindow.ToastNotify("Cross scheme redirect from Gemini not supported: " + redirectUri, ToastMessageStyles.Warning); mMainWindow.ToggleContainerControlsForBrowser(true); e.Cancel = true; return; } else { //others e.g. http->https redirect are fine } //redirected to a full gemini url geminiUri = redirectUri; //regenerate the hashes using the redirected target url hash = HashService.GetMd5Hash(geminiUri); var gmiFileNew = sessionPath + "\\" + hash + ".txt"; var htmlFileNew = sessionPath + "\\" + hash + ".htm"; //move the source file try { if (File.Exists(gmiFileNew)) { File.Delete(gmiFileNew); } File.Move(gmiFile, gmiFileNew); } catch (Exception err) { mMainWindow.ToastNotify(err.ToString(), ToastMessageStyles.Error); } //update locations of gmi and html file gmiFile = gmiFileNew; htmlFile = htmlFileNew; } else { geminiUri = fullQuery; } var userThemesFolder = ResourceFinder.LocalOrDevFolder(appDir, @"GmiConverters\themes", @"..\..\..\GmiConverters\themes"); var userThemeBase = Path.Combine(userThemesFolder, settings.Theme); mMainWindow.ShowUrl(geminiUri, gmiFile, htmlFile, userThemeBase, siteIdentity, e); } } // codemajor = 3 is redirect - should eventually end in success or raise an error else if (geminiResponse.codeMajor == '4') { mMainWindow.ToastNotify("Temporary failure (status 4X)\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } else if (geminiResponse.codeMajor == '5') { if (geminiResponse.codeMinor == '1') { mMainWindow.ToastNotify("Page not found\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } else { mMainWindow.ToastNotify("Permanent failure (status 5X)\n\n" + geminiResponse.meta + "\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } } else if (geminiResponse.codeMajor == '6') { mMainWindow.ToastNotify("Certificate requried. Choose one and try again.\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } else { mMainWindow.ToastNotify("Unexpected output from server " + "(status " + geminiResponse.codeMajor + "." + geminiResponse.codeMinor + ") " + geminiResponse.meta + "\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } } catch (Exception err) { //generic handler for other runtime errors mMainWindow.ToastNotify("Error getting gemini content for " + e.Uri.ToString() + "\n\n" + err.Message, ToastMessageStyles.Warning); } //make the window responsive again mMainWindow.ToggleContainerControlsForBrowser(true); //no further navigation right now e.Cancel = true; }
public bool NavigateGeminiScheme(string fullQuery, Uri uri, SiteIdentity siteIdentity, bool requireSecure = true) { bool navigated = true; var geminiUri = uri.OriginalString; var sessionPath = Session.Instance.SessionPath; var appDir = AppDomain.CurrentDomain.BaseDirectory; //use local or dev binary for gemget var gemGet = ResourceFinder.LocalOrDevFile(appDir, "Gemget", "..\\..\\..\\..\\Gemget", "gemget-windows-386.exe"); var hash = HashService.GetMd5Hash(fullQuery); //uses .txt as extension so content loaded as text/plain not interpreted by the browser //if user requests a view-source. var rawFile = sessionPath + "\\" + hash + ".txt"; var gmiFile = sessionPath + "\\" + hash + ".gmi"; var htmlFile = sessionPath + "\\" + hash + ".htm"; //delete txt file as GemGet seems to sometimes overwrite not create afresh File.Delete(rawFile); File.Delete(gmiFile); //delete any existing html file to encourage webbrowser to reload it File.Delete(htmlFile); var settings = new Settings(); string command = ""; var secureFlag = requireSecure ? "" : " -i "; if (uri.Scheme == "gemini") { //pass options to gemget for download command = string.Format( "\"{0}\" {1} --header --no-progress-bar -m \"{2}\"Mb -t {3} -o \"{4}\" \"{5}\"", gemGet, secureFlag, settings.MaxDownloadSizeMb, settings.MaxDownloadTimeSeconds, rawFile, fullQuery); } else { //pass options to gemget for download using the assigned http proxy, such as //duckling-proxy https://github.com/LukeEmmet/duckling-proxy //this should obviously be a trusted server since it is in the middle of the //request command = string.Format( "\"{0}\" {1} --header --no-progress-bar -m \"{2}\"Mb -t {3} -o \"{4}\" -p \"{5}\" \"{6}\"", gemGet, secureFlag, settings.MaxDownloadSizeMb, settings.MaxDownloadTimeSeconds, rawFile, settings.HttpSchemeProxy, fullQuery); } var result = ExecuteProcess.ExecuteCommand(command, true, true); var geminiResponse = new Response.GeminiResponse(fullQuery); geminiResponse.ParseGemGet(result.Item2); //parse stdout geminiResponse.ParseGemGet(result.Item3); //parse stderr //ToastNotify(geminiResponse.Status + " " + geminiResponse.Meta); //in these early days of Gemini we dont forbid visiting a site with an expired cert or mismatched host name //but we do give a warning each time if (result.Item1 == 1 && requireSecure) { var tryInsecure = false; var securityError = ""; if (geminiResponse.Errors[0].Contains("server cert is expired")) { tryInsecure = true; securityError = "Server certificate is expired"; } else if (geminiResponse.Errors[0].Contains("hostname does not verify")) { tryInsecure = true; securityError = "Host name does not verify"; } if (tryInsecure) { //give a warning and try again with insecure mMainWindow.ToastNotify("Note: " + securityError + " for: " + uri.Authority, ToastMessageStyles.Warning); NavigateGeminiScheme(fullQuery, uri, siteIdentity, false); return(true); } } if (geminiResponse.AbandonedTimeout || geminiResponse.AbandonedSize) { var abandonMessage = string.Format( "Download was abandoned as it exceeded the max size ({0}) or time ({1} s). See GemiNaut settings for details.\n\n{2}", settings.MaxDownloadSizeMb, settings.MaxDownloadTimeSeconds, fullQuery); mMainWindow.ToastNotify(abandonMessage, ToastMessageStyles.Warning); mMainWindow.ToggleContainerControlsForBrowser(true); return(false); } if (File.Exists(rawFile)) { if (geminiResponse.Meta.Contains("text/gemini")) { File.Copy(rawFile, gmiFile); } else if (geminiResponse.Meta.Contains("text/html")) { //is an html file served over gemini - probably not common, but not unheard of var htmltoGmiResult = ConverterService.HtmlToGmi(rawFile, gmiFile); if (htmltoGmiResult.Item1 != 0) { mMainWindow.ToastNotify("Could not convert HTML to GMI: " + fullQuery, ToastMessageStyles.Error); mMainWindow.ToggleContainerControlsForBrowser(true); return(false); } } else if (geminiResponse.Meta.Contains("text/")) { //convert plain text to a gemini version (wraps it in a preformatted section) var textToGmiResult = ConverterService.TextToGmi(rawFile, gmiFile); if (textToGmiResult.Item1 != 0) { mMainWindow.ToastNotify("Could not render text as GMI: " + fullQuery, ToastMessageStyles.Error); mMainWindow.ToggleContainerControlsForBrowser(true); return(false); } } else { //a download //its an image - rename the raw file and just show it var pathFragment = (new UriBuilder(fullQuery)).Path; var ext = Path.GetExtension(pathFragment); var binFile = rawFile + (ext == "" ? ".tmp" : ext); File.Copy(rawFile, binFile, true); //rename overwriting if (geminiResponse.Meta.Contains("image/")) { mMainWindow.ShowImage(fullQuery, binFile); } else { SaveFileDialog saveFileDialog = new SaveFileDialog(); saveFileDialog.FileName = Path.GetFileName(pathFragment); if (saveFileDialog.ShowDialog() == true) { try { //save the file var savePath = saveFileDialog.FileName; File.Copy(binFile, savePath, true); //rename overwriting mMainWindow.ToastNotify("File saved to " + savePath, ToastMessageStyles.Success); } catch (SystemException err) { mMainWindow.ToastNotify("Could not save the file due to: " + err.Message, ToastMessageStyles.Error); } } mMainWindow.ToggleContainerControlsForBrowser(true); return(false); } return(true); } if (geminiResponse.Redirected) { string redirectUri = fullQuery; if (geminiResponse.FinalUrl.Contains("://")) { //a full url //normalise the URi (e.g. remove default port if specified) redirectUri = UriTester.NormaliseUri(new Uri(geminiResponse.FinalUrl)).ToString(); } else { //a relative one var baseUri = new Uri(fullQuery); var targetUri = new Uri(baseUri, geminiResponse.FinalUrl); redirectUri = UriTester.NormaliseUri(targetUri).ToString(); } var finalUri = new Uri(redirectUri); if (uri.Scheme == "gemini" && finalUri.Scheme != "gemini") { //cross-scheme redirect, not supported mMainWindow.ToastNotify("Cross scheme redirect from Gemini not supported: " + redirectUri, ToastMessageStyles.Warning); mMainWindow.ToggleContainerControlsForBrowser(true); return(false); } else { //others e.g. http->https redirect are fine } //redirected to a full gemini url geminiUri = redirectUri; //regenerate the hashes using the redirected target url hash = HashService.GetMd5Hash(geminiUri); var gmiFileNew = sessionPath + "\\" + hash + ".txt"; var htmlFileNew = sessionPath + "\\" + hash + ".htm"; //move the source file try { if (File.Exists(gmiFileNew)) { File.Delete(gmiFileNew); } File.Move(gmiFile, gmiFileNew); } catch (Exception err) { mMainWindow.ToastNotify(err.ToString(), ToastMessageStyles.Error); } //update locations of gmi and html file gmiFile = gmiFileNew; htmlFile = htmlFileNew; } else { geminiUri = fullQuery; } var userThemesFolder = ResourceFinder.LocalOrDevFolder(appDir, @"GmiConverters\themes", @"..\..\GmiConverters\themes"); var userThemeBase = Path.Combine(userThemesFolder, settings.Theme); mMainWindow.ShowUrl(geminiUri, gmiFile, htmlFile, userThemeBase, siteIdentity); } else if (geminiResponse.Status == 10 || geminiResponse.Status == 11) { //needs input mMainWindow.ToggleContainerControlsForBrowser(true); navigated = NavigateGeminiWithInput(uri, geminiResponse.Meta); } else if (geminiResponse.Status == 50 || geminiResponse.Status == 51) { mMainWindow.ToastNotify("Page not found (status 51)\n\n" + uri.ToString(), ToastMessageStyles.Warning); } else { //some othe error - show to the user for info mMainWindow.ToastNotify(string.Format( "Cannot retrieve the content (exit code {0}): \n\n{1} \n\n{2}", result.Item1, string.Join("\n\n", geminiResponse.Info), string.Join("\n\n", geminiResponse.Errors) ), ToastMessageStyles.Error); } mMainWindow.ToggleContainerControlsForBrowser(true); //no further navigation right now return(navigated); }
public void NavigateNimigemScheme(string fullQuery, System.Windows.Navigation.NavigatingCancelEventArgs e, string payload, bool requireSecure = true) { var NimigemUri = e.Uri.OriginalString; //at present we only support UTF8 plain text payloads byte[] nimigemBody = Encoding.UTF8.GetBytes(payload); var mime = "text/plain; charset=utf-8"; var settings = new UserSettings(); var uri = new Uri(fullQuery); //use a proxy for any other scheme that is not Nimigem var proxy = ""; //use none var connectInsecure = false; if (uri.Host == "localhost") { //to support local testing servers, dont require secure connection on localhost //**FIX ME, or have an option connectInsecure = true; } X509Certificate2 certificate; certificate = Session.Instance.CertificatesManager.GetCertificate(uri.Host); try { NimigemResponse nimigemResponse; try { nimigemResponse = (NimigemResponse)Nimigem.Fetch(uri, nimigemBody, mime, certificate, proxy, connectInsecure, settings.MaxDownloadSizeMb * 1024, settings.MaxDownloadTimeSeconds); } catch (Exception err) { //warn, but continue if there are server validation errors //in these early days of Nimigem we dont forbid visiting a site with an expired cert or mismatched host name //but we do give a warning each time if (err.Message == "The remote certificate was rejected by the provided RemoteCertificateValidationCallback.") { mMainWindow.ToastNotify("Note: " + err.Message + " for: " + e.Uri.Authority, ToastMessageStyles.Warning); //try again insecure this time nimigemResponse = (NimigemResponse)Nimigem.Fetch(uri, nimigemBody, mime, certificate, proxy, connectInsecure, settings.MaxDownloadSizeMb * 1024, settings.MaxDownloadTimeSeconds); } else { //reraise throw; } } if (nimigemResponse.codeMajor == '1') { //invalid in nimigem HandleInvalidResponse(nimigemResponse); } else if (nimigemResponse.codeMajor == '2') { //success if (nimigemResponse.codeMinor == '5') { //valid submission - get the new target to retrieve mMainWindow.ToastNotify(String.Format("Submit successful: retrieving result: {0}", nimigemResponse.meta)); var successUri = new Uri(nimigemResponse.meta); //must be a response redirect to gemini URL if (successUri.Scheme != "gemini") { HandleInvalidResponse(nimigemResponse); } var geminiTarget = new GeminiNavigator(mMainWindow, mMainWindow.BrowserControl); var normalisedUri = UriTester.NormaliseUri(successUri); var siteIdentity = new SiteIdentity(normalisedUri, Session.Instance); geminiTarget.NavigateGeminiScheme(successUri.OriginalString, e, siteIdentity); } else { //no other 2X responses are valid HandleInvalidResponse(nimigemResponse); } } // codemajor = 3 is redirect - should eventually end in success or raise an error else if (nimigemResponse.codeMajor == '4') { //same as normal Gemini mMainWindow.ToastNotify("Temporary failure (status 4X)\n\n" + nimigemResponse.meta + "\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } else if (nimigemResponse.codeMajor == '5') { //same as normal Gemini if (nimigemResponse.codeMinor == '1') { mMainWindow.ToastNotify("Page not found\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } else { mMainWindow.ToastNotify("Permanent failure (status 5X)\n\n" + nimigemResponse.meta + "\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } } else if (nimigemResponse.codeMajor == '6') { mMainWindow.ToastNotify("Certificate required. Choose one and try again.\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } else { mMainWindow.ToastNotify("Unexpected output from server " + "(status " + nimigemResponse.codeMajor + "." + nimigemResponse.codeMinor + ") " + nimigemResponse.meta + "\n\n" + e.Uri.ToString(), ToastMessageStyles.Warning); } } catch (Exception err) { //generic handler for other runtime errors mMainWindow.ToastNotify("Error getting Nimigem content for " + e.Uri.ToString() + "\n\n" + err.Message, ToastMessageStyles.Warning); } //make the window responsive again mMainWindow.ToggleContainerControlsForBrowser(true); //no further navigation right now e.Cancel = true; }
private void BrowserControl_Navigating(object sender, System.Windows.Navigation.NavigatingCancelEventArgs e) { var normalisedUri = UriTester.NormaliseUri(e.Uri); var siteIdentity = new SiteIdentity(normalisedUri, Session.Instance); var fullQuery = normalisedUri.OriginalString; //sanity check we have a valid URL syntax at least if (e.Uri.Scheme == null) { ToastNotify("Invalid URL: " + normalisedUri.OriginalString, ToastMessageStyles.Error); e.Cancel = true; } var settings = new Settings(); ToggleContainerControlsForBrowser(false); //these are the only ones we "navigate" to. We do this by downloading the GMI content //converting to HTML and then actually navigating to that. if (normalisedUri.Scheme == "gemini") { var geminiNavigator = new GeminiNavigator(this, this.BrowserControl); geminiNavigator.NavigateGeminiScheme(fullQuery, e, siteIdentity); } else if (normalisedUri.Scheme == "gopher") { var gopherNavigator = new GopherNavigator(this, this.BrowserControl); gopherNavigator.NavigateGopherScheme(fullQuery, e, siteIdentity); } else if (normalisedUri.Scheme == "about") { var aboutNavigator = new AboutNavigator(this, this.BrowserControl); aboutNavigator.NavigateAboutScheme(e, siteIdentity); } else if (normalisedUri.Scheme.StartsWith("http")) //both http and https { var linkId = ""; ////doc might be null - you need to check when using! var doc = (HTMLDocument)BrowserControl.Document; ////this is how we could detect a click on a link to an image... if (doc?.activeElement != null) { linkId = doc.activeElement.id; } //detect ctrl click if ( Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl) || settings.HandleWebLinks == "System web browser" || linkId == "web-launch-external" ) { //open in system web browser var launcher = new ExternalNavigator(this); launcher.LaunchExternalUri(e.Uri.ToString()); ToggleContainerControlsForBrowser(true); e.Cancel = true; } else if (settings.HandleWebLinks == "Gemini HTTP proxy") { // use a gemini proxy for http links var geminiNavigator = new GeminiNavigator(this, this.BrowserControl); geminiNavigator.NavigateGeminiScheme(fullQuery, e, siteIdentity); } else { //use internal navigator var httpNavigator = new HttpNavigator(this, this.BrowserControl); httpNavigator.NavigateHttpScheme(fullQuery, e, siteIdentity, linkId); } } else if (normalisedUri.Scheme == "file") { //just load the converted html file //no further action. } else { //we don't care about any other protocols //so we open those in system web browser to deal with var launcher = new ExternalNavigator(this); launcher.LaunchExternalUri(e.Uri.ToString()); ToggleContainerControlsForBrowser(true); e.Cancel = true; } }