Toodledo iCal v2

Friday, 23 October 2009 06:04 PM
by Coose

I’m going to make this short, because I just typed up a full post and the piece of shit Windows Live Writer crashed…again…and I lost everything and don’t feel like typing it all again.

In a previous post, I described an iCal feed that I created to enhance the functionality of Toodledo’s iCal feed.  To restate the problem: The Toodledo iCal feed creates an all-day event for each task that has a due date.  The problem is that if a task overdue and not completed, it should show up on the current day, not the original due date.  Windows Mobile applications like Agenda One and Pocket Informant use this functionality, and I really got used to it.  Toodledo tasks are always displayed on the calendar feed on their original due date.

So, what I need to do is if the task is incomplete and overdue, modify the due date to today.  At first glance I thought of using the Toodledo API to create the calendar feed manually.  The problem is I have three young kids at home, and a million things on my plate.  So I need a quick and dirty solution.

What I originally opted to do is create a handler on my own web site to serve up the ics feed.  The handler would call the Toodledo ics feed, and modify any overdue start and end date in the resulting stream and set it to the current date.  Quick, easy, and effective.  Perfect.  Examining the ics feed manually, and testing in Sunbird and Outlook showed everything A-OK.

But when adding the feed to Google, it originally shows up correctly, but overdue tasks do not show up on the current day, like I coded.  Google does not allow a refresh of the subscribed calendars, so there’s no way to test immediately.  But after watching a few days, it seems that the calendar appears to cached on Google servers.  I think what I need to do is modify the start and end date of expired incomplete tasks, as well as the last modified date.  This should cue Google to update the calendar item.

So, the first thing to do is see what the original feed looks like.  Using one of the most useful tools in my toolbox, LINQPad, I we can see the resulting stream.  The address of the ics from Toodledo can be found by logging in to Toodledo, and navigating to http://www.toodledo.com/connect_ical.php.

The LINQPad statement to show the result is:
Encoding.ASCII.GetString(new WebClient().DownloadData("http://www.toodledo.com/id/tdXXXXXXXXXXXXX/ical_live.ics")).Dump();
Again, your URL will differ.  Make sure to change the “webcal://” protocol to “http://” as WebClient doesn’t understand what “webcal://” is.

The resulting data is:
BEGIN:VCALENDAR
PRODID:-//Toodledo iCal//www.toodledo.com//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Toodledo iCal
LAST-MODIFIED:20091023T220858Z
BEGIN:VEVENT
URL:http://www.toodledo.com
CREATED:20090822T000000
DTSTAMP:20090822T000000
LAST-MODIFIED:20090927T205903
UID:some-guid#toodledo.com
SUMMARY:Task Name
DESCRIPTION:
PRIORITY:5
STATUS:CONFIRMED
TRANSP:OPAQUE
DTSTART;VALUE=DATE:20090927
DTEND;VALUE=DATE:20090927

END:VEVENT
END:VCALENDAR

Toodledo only returns incomplete tasks in its feed, so we don’t have to worry about filtering on the status.  The lines in red are the lines that need to be modified for our customized feed.

So, create a handler for your feed name derived from IHttpHandler.  Override the ProcessRequest method to parse each VEVENT and if needed, adjust the dates.

    1 public void ProcessRequest(HttpContext context)

    2 {

    3     WebClient client = new WebClient();

    4     string toodledo = Encoding.ASCII.GetString(client.DownloadData("http://www.toodledo.com/id/tdXXXXXXXXXXXXX/ical_live.ics"));

    5 

    6     List<Dictionary<string, string>> items = new List<Dictionary<string, string>>();

    7 

    8     foreach (Match match in Regex.Matches(toodledo, @"BEGIN:VEVENT.*?END:VEVENT", RegexOptions.Singleline))

    9     {

   10         Dictionary<string, string> values = new Dictionary<string, string>();

   11 

   12         // now we have a bunch of name/value pairs

   13         foreach (string line in match.Value.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))

   14         {

   15             int x = line.IndexOf(':');

   16             if (x < 0)

   17             {

   18                 values[line] = string.Empty;

   19             }

   20             else

   21             {

   22                 values[line.Substring(0, x)] = (x >= line.Length - 1) ? string.Empty : line.Substring(x + 1);

   23             }

   24         }

   25 

   26         items.Add(values);

   27     }

   28 

   29     // adjust dates.

   30     // when adjusting the date, make sure to adjust modified date, etc.  Google seems to need this.

   31     foreach (Dictionary<string, string> item in items)

   32     {

   33         string[] keys = new string[] { "DTSTART;VALUE=DATE", "DTEND;VALUE=DATE" };

   34         foreach (string key in keys)

   35         {

   36             if (item.ContainsKey(key))

   37             {

   38                 DateTime currentDate = DateTime.ParseExact(item[key], "yyyyMMdd", CultureInfo.InvariantCulture);

   39                 if (currentDate < DateTime.Now)

   40                 {

   41                     item[key] = DateTime.Now.ToString("yyyyMMdd");

   42                     item["LAST-MODIFIED"] = DateTime.Now.ToString("yyyyMMddThhmmss");

   43                     item["DTSTAMP"] = DateTime.Now.ToString("yyyyMMddThhmmss");

   44                 }

   45             }

   46         }

   47     }

   48 

   49     StringBuilder sb = new StringBuilder();

   50 

   51     sb.AppendLine("BEGIN:VCALENDAR");

   52     sb.AppendLine("PRODID:-//Toodledo iCal//www.toodledo.com//EN");

   53     sb.AppendLine("VERSION:2.0");

   54     sb.AppendLine("CALSCALE:GREGORIAN");

   55     sb.AppendLine("METHOD:PUBLISH");

   56     sb.AppendLine("X-WR-CALNAME:Toodledo iCal");

   57     sb.Append("LAST-MODIFIED:");

   58     sb.Append(DateTime.UtcNow.ToString("yyyyMMddThhmmssZ"));

   59     sb.AppendLine();

   60 

   61     foreach (Dictionary<string, string> item in items)

   62     {

   63         foreach (KeyValuePair<string, string> pair in item)

   64         {

   65             sb.AppendFormat("{0}:{1}", pair.Key, pair.Value);

   66             sb.AppendLine();

   67         }

   68     }

   69 

   70     sb.AppendLine("END:VCALENDAR");

   71 

   72     context.Response.Cache.SetCacheability(HttpCacheability.NoCache);

   73     context.Response.Headers["Keep-Alive"] = "timeout=2, max=100";

   74     context.Response.Headers["Connection"] = "Keep-Alive";

   75     context.Response.ContentType = client.ResponseHeaders[HttpResponseHeader.ContentType];

   76     context.Response.Output.Write(sb.ToString());

   77 }

A quick overview of what is happening:

  • Line 4 downloads the full original feed from Toodledo.
  • Lines 6 – 27 find all text between “BEGIN:VEVENT” and “END:VEVENT”, and for each line adds a dictionary entry with the text to the left of the : as the key, and to the right as the value.
  • Lines 31 – 47 find any DTSTART;VALUE=DATE or DTEND;VALUE=DATE entries, parses the value into a date, and changes the date to today if the date is in the past, and also sets LAST-MODIFIED and DTSTAMP to today.
  • Lines 51 – 59 write the standard iCal information.
  • Line 58 changes the modified date of the entire iCal feed to today.
  • Lines 61 – 68 write each name/value pair of the dictionary back to the iCal.
  • Line 70 closes the iCal.
  • Lines 72 – 76 writes the iCal to the response stream.

Publish this handler on your web site, and add the URL of YOUR HANDLER to Google calendar, and your Toodledo tasks are “fixed”.

Comment on this
Mike's Blog
|

Better Toodledo iCal

Saturday, 12 September 2009 05:15 PM
by Coose

UPDATE: This post is outdated.  See this post for an updated version.

Since the iPhone doesn’t have tasks at all, I had to find an external service to handle this.  I started with Remember-the-Milk, but was thoroughly unimpressed.  I stumbled across Toodledo and liked what they had to offer.  What I liked was:

  • Web access
  • Native iPhone application
  • Automatic and nearly transparent sync from iPhone back to web service
  • ICS feed for my Google and iPhone calendars

The last item is what I’m going to talk about today.  When using Windows Mobile, I DX’d the built-in calendar for Pocket Informant, then later for Agenda One.  One of the things I liked the best about those PIMs was that on the current day, it showed tasks that are due today as well as those that are overdue.  That last part is important.  If I had a task due four days ago, and I didn’t do it and check it off, I need to see it on my calendar TODAY.  Not when it was originally due.  Again, this is only true for OVERDUE tasks.  This functionality was not in Toodledo. :(

Toodledo has an ICS feed, which is great.  It allows me to show tasks on my Google calendar, as well as on my iPhone calendar.  They are not ICS task items, they are done as all-day events.  That’s cool because iPhone’s iCal and Google calendar don’t display ICS tasks correctly anyway.  BUT, Toodledo just uses the raw due date on the task for the start and end date of the ICS event.  This has been driving me nuts, so I decided to make a change myself.

My first attempt was to access the Toodledo APIs over the wire directly, and generate my own ICS feed based on the raw tasks.  I created a C# library, that I was also using for a Silverlight Toodledo interface that I’ve been tinkering with.  But I do have a handful of kids, and a full time job, so I decided on an easier way.

I figured that Toodledo is already generating an ICS feed, so let’s take their feed, and for any start and end date that is older than today, just switch it to today.  That way all overdue tasks will continue to show up on today’s date, whatever day today is.  Now I have a more constant reminder to get those overdue tasks DONE!!!

So, I created a new class in my own web site’s App_Code directory that derives from IHttpHandler.  Register that handler in the web.config of my web site for a specific file name, for example ‘mytasks.ics’ (don’t try to access that on my web site, that’s not what I really called it).  The handler uses a WebClient class to retrieve the data from Toodledo, then a regular expression to find expired dates and replace them with today.  I then write that data to the response stream, and viola!

Here’s my class:

    1 using System;

    2 using System.Collections.Generic;

    3 using System.Linq;

    4 using System.Web;

    5 using System.Net;

    6 using System.Text;

    7 using System.Text.RegularExpressions;

    8 using System.Globalization;

    9 using System.IO;

   10 

   11 namespace Funkymule

   12 {

   13     public class ToodledoHandler : IHttpHandler

   14     {

   15         #region IHttpHandler Members

   16 

   17         public bool IsReusable

   18         {

   19             get { return true; }

   20         }

   21 

   22         public void ProcessRequest(HttpContext context)

   23         {

   24             WebClient client = new WebClient();

   25             byte[] buffer = client.DownloadData("http://www.toodledo.com/path/to/your/toodledoo/feed.ics");

   26             StringBuilder ics = new StringBuilder(Encoding.Default.GetString(buffer));

   27 

   28             Regex rx = new Regex(@"^(DTSTART|DTEND);VALUE=DATE:(?<date>.*)\s*$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Multiline);

   29             foreach (Match m in rx.Matches(ics.ToString()))

   30             {

   31                 Group g = m.Groups["date"];

   32                 DateTime endDate = DateTime.ParseExact(g.Value, "yyyyMMdd", CultureInfo.InvariantCulture);

   33                 if (endDate < DateTime.Now)

   34                 {

   35                     ics.Remove(g.Index, g.Length);

   36                     ics.Insert(g.Index, DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture));

   37                 }

   38             }

   39 

   40             context.Response.ContentType = client.ResponseHeaders[HttpResponseHeader.ContentType];

   41             context.Response.Output.Write(ics.ToString());

   42         }

   43 

   44         #endregion

   45     }

   46 }

I put this in my App_Code directory, then added the handler in the web.config:

<system.webServer>

    <handlers>

        <add name="MyTasks" verb="*" path="mytasks.ics" type="Funkymule.ToodledoHandler" />

    </handlers>

</system.webServer>

If you are using a version of IIS older than 7, your configuration will be slightly different from mine.

Now all lines of the Toodledo issued ICS response that are like this:

DTSTART;VALUE=DATE:20090901
DTEND;VALUE=DATE:20090901

Which says that this task was due on September 1, will be changed to:

DTSTART;VALUE=DATE:20090912
DTEND;VALUE=DATE:20090912

because today is the 12th and the example above is an overdue task.  Now when subscribing to this calendar in whatever iCal type app you want (iCal, Google calendar, iPhone calendar, Outlook, Sunbird/Lightning) any overdue tasks will show up as all-day event on the current day.

Quick and easy, and I didn’t have to write a line of iPhone code to get the iPhone to work the way I like it.  Even though the built-in iPhone calendar blows really bad, the ActiveSync/CalDAV/iCal flexibility is a homerun.

UPDATE: This post is outdated.  See this post for an updated version.

Comment on this
Mike's Blog
|

Internet Proxy Monitor

Wednesday, 09 September 2009 07:12 PM
by Coose

Our company enabled the Internet Explorer Proxy via group policy a while back to “improve performance”.  The improvement is about 10 times slower than it was before.  They insist that it is faster, but they are WRONG (yes Ken and Kevin…I’m talking about you).  Not only is it much slower, but the “bypass proxy on local addresses” doesn’t work right, so our WCF endpoints don’t work correctly without configuring them for the proxy (which we can only use message security if we do this).

[more Read on for my solution…]

Our group policy typically has these options disabled.  So it’s quite frustrating to need to turn that off for WCF.  Obviously FireFox fixed the problem for web browsing, but for WCF, this was still a problem.

So, I wrote a C# program to poll the Internet Options proxy settings, and notify me when the settings are changed, display an icon on the tray to remind me, and gives me a one-click option to toggle the proxy off when needed, then back on.  It’s not a terribly complicated program, but it demonstrates two interesting points:

  1. P/Invoke variable sized arrays
  2. WinForms application with NotifyIcon without having a Form at all by using a custom ApplicationContext.

So, to create a Formless Windows Forms Application, create a new Windows Forms Application, and remove the form.  Then create a new class deriving from ApplicationContext.  Add relevant code.  My proxy code looks like this:

    1 class ProxyMonitorApplicationContext : ApplicationContext

    2 {

    3     private Container _components;

    4     private NotifyIcon _notifyIcon;

    5     private ContextMenu _menu;

    6     private Timer _timer;

    7     private bool? _enabled;

    8 

    9     public ProxyMonitorApplicationContext()

   10     {

   11         this.InitializeContext();   

   12     }

   13 

   14     protected void InitializeContext()

   15     {

   16         _components = new Container();

   17 

   18         _menu = new ContextMenu(new MenuItem[] {

   19             new MenuItem(Resources.ToggleMenu, new EventHandler(this.Menu_ToggleClicked)),

   20             new MenuItem(Resources.ExitMenu, new EventHandler(this.Menu_ExitClicked))

   21             }

   22         );

   23 

   24         _notifyIcon = new NotifyIcon(_components);

   25         _notifyIcon.Visible = true;

   26         _notifyIcon.MouseClick += new MouseEventHandler(NotifyIcon_MouseClick);

   27         _notifyIcon.BalloonTipClicked += new EventHandler(NotifyIcon_BalloonTipClicked);

   28         _notifyIcon.ContextMenu = _menu;

   29 

   30         _timer = new Timer(_components);

   31         _timer.Enabled = true;

   32         _timer.Interval = Settings.Default.QueryInterval;

   33         _timer.Tick += new EventHandler(Timer_Tick);

   34 

   35         UpdateStatus(false);

   36     }

   37 

   38     protected override void Dispose(bool disposing)

   39     {

   40         if (disposing && (this._components != null))

   41         {

   42             this._components.Dispose();

   43             _notifyIcon.Dispose();

   44             _timer.Dispose();

   45             _menu.Dispose();

   46         }

   47         base.Dispose(disposing);

   48     }

   49 

   50     private void Timer_Tick(object sender, EventArgs e)

   51     {

   52         bool enabled = InternetOptionsProxy.IsProxyEnabled();

   53         if (_enabled != enabled)

   54         {

   55             UpdateStatus(true);

   56             ShowNotification();

   57         }

   58         _enabled = enabled;

   59     }

   60 

   61     void NotifyIcon_MouseClick(object sender, MouseEventArgs e)

   62     {

   63         if (e.Button == MouseButtons.Left)

   64         {

   65             UpdateStatus(false);

   66             ShowNotification();

   67         }

   68     }

   69 

   70     void NotifyIcon_BalloonTipClicked(object sender, EventArgs e)

   71     {

   72         ToggleStatus();

   73     }

   74 

   75     void Menu_ToggleClicked(object sender, EventArgs e)

   76     {

   77         ToggleStatus();

   78     }

   79 

   80     void Menu_ExitClicked(object sender, EventArgs e)

   81     {

   82         this.ExitThread();

   83     }

   84 

   85     private void ShowNotification()

   86     {

   87         _notifyIcon.BalloonTipTitle = (this._enabled.HasValue && this._enabled.Value)

   88             ? Resources.EnabledTitle

   89             : Resources.DisabledTitle;

   90         _notifyIcon.BalloonTipText = (this._enabled.HasValue && this._enabled.Value)

   91             ? Resources.EnabledMessage

   92             : Resources.DisabledMessage;

   93         _notifyIcon.BalloonTipIcon = (this._enabled.HasValue && this._enabled.Value)

   94             ? ToolTipIcon.Warning

   95             : ToolTipIcon.Info;

   96         _notifyIcon.ShowBalloonTip(Settings.Default.NotificationDuration);

   97     }

   98 

   99     private void ToggleStatus()

  100     {

  101         InternetOptionsProxy.EnableProxy((_enabled == null || _enabled.Value == false));

  102         UpdateStatus(false);

  103     }

  104 

  105     private void UpdateStatus(bool popupIfChanged)

  106     {

  107         bool enabled = InternetOptionsProxy.IsProxyEnabled();

  108         bool changed = _enabled != enabled;

  109         if (!changed) return;

  110 

  111         _enabled = enabled;

  112         if (changed && popupIfChanged)

  113         {

  114             ShowNotification();

  115         }

  116 

  117         _notifyIcon.Text = (this._enabled.HasValue && this._enabled.Value)

  118             ? Resources.EnabledTitle

  119             : Resources.DisabledTitle;

  120         _notifyIcon.BalloonTipTitle = _notifyIcon.Text;

  121         _notifyIcon.BalloonTipText = (this._enabled.HasValue && this._enabled.Value)

  122             ? Resources.EnabledMessage

  123             : Resources.DisabledMessage;

  124         _notifyIcon.Icon = new Icon(

  125             (this._enabled.HasValue && this._enabled.Value)

  126             ? Resources.ProxyEnabled :

  127             Resources.ProxyDisabled, new Size(16, 16)

  128         );

  129     }

  130 }

Essentially, just make an InitializeComponent method, to create an initialize the components that are typically sited on a Form, and be sure to override the Dispose method.

Then create a Program.cs or whatever class you like for the static Main method.  In the Application.Run method, pass a new instance of your context instead of the default form.

    1 [STAThread]

    2 static void Main()

    3 {

    4     Application.EnableVisualStyles();

    5     Application.SetCompatibleTextRenderingDefault(false);

    6     Application.Run(new ProxyMonitorApplicationContext());

    7 }

 

Now, we will P/Invoke methods to actually turn on and off the Internet proxy server.  So here’s the P/Invoke signatures that I used:

    1 internal static class NativeMethods

    2 {

    3     [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

    4     public class INTERNET_PER_CONN_OPTION_LIST

    5     {

    6         public int dwSize;

    7         [MarshalAs(UnmanagedType.LPWStr)]

    8         public string pszConnection;

    9         public int dwOptionCount;

   10         public int dwOptionError;

   11         public IntPtr pOptions;

   12     }

   13 

   14     [StructLayout(LayoutKind.Explicit, Size = 12)]

   15     public struct INTERNET_PER_CONN_OPTION

   16     {

   17         // Fields

   18         [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]

   19         [FieldOffset(0)]

   20         public int dwOption;

   21         [FieldOffset(4)]

   22         public IntPtr ptrValue;

   23     }

   24 

   25     public const int PROXY_TYPE_AUTO_DETECT = 8;

   26     public const int PROXY_TYPE_AUTO_PROXY_URL = 4;

   27     public const int PROXY_TYPE_DIRECT = 1;

   28     public const int PROXY_TYPE_PROXY = 2;

   29     public const int INTERNET_PER_CONN_AUTOCONFIG_LAST_DETECT_TIME = 8;

   30     public const int INTERNET_PER_CONN_AUTOCONFIG_LAST_DETECT_URL = 9;

   31     public const int INTERNET_PER_CONN_AUTOCONFIG_RELOAD_DELAY_MINS = 7;

   32     public const int INTERNET_PER_CONN_AUTOCONFIG_SECONDARY_URL = 6;

   33     public const int INTERNET_PER_CONN_AUTOCONFIG_URL = 4;

   34     public const int INTERNET_PER_CONN_AUTODISCOVERY_FLAGS = 5;

   35     public const int INTERNET_PER_CONN_FLAGS = 1;

   36     public const int INTERNET_PER_CONN_PROXY_BYPASS = 3;

   37     public const int INTERNET_PER_CONN_PROXY_SERVER = 2;

   38     public const int INTERNET_OPTION_PER_CONNECTION_OPTION = 0x4b;

   39     public const int INTERNET_OPTION_REFRESH = 0x25;

   40 

   41     [DllImport("kernel32.dll")]

   42     public static extern IntPtr GlobalFree(IntPtr hMem);

   43     [DllImport("wininet.dll", SetLastError = true)]

   44     [return: MarshalAs(UnmanagedType.Bool)]

   45     public static extern bool InternetQueryOption(IntPtr hInternet, uint dwOption, INTERNET_PER_CONN_OPTION_LIST lpBuffer, ref int lpdwBufferLength);

   46     [DllImport("wininet.dll")]

   47     [return: MarshalAs(UnmanagedType.Bool)]

   48     public static extern bool InternetSetOption(IntPtr hInternet, int dwOption, INTERNET_PER_CONN_OPTION_LIST lpBuffer, int dwBufferLength);

   49 }

And a helper class to call the methods from a nicer .NET friendly API:

    1 internal static class InternetOptionsProxy

    2 {

    3     public static bool IsProxyEnabled()

    4     {

    5         NativeMethods.INTERNET_PER_CONN_OPTION_LIST list = new NativeMethods.INTERNET_PER_CONN_OPTION_LIST();

    6         NativeMethods.INTERNET_PER_CONN_OPTION[] options = new NativeMethods.INTERNET_PER_CONN_OPTION[5];

    7         options[0].dwOption = NativeMethods.INTERNET_PER_CONN_AUTOCONFIG_URL;

    8         options[1].dwOption = NativeMethods.INTERNET_PER_CONN_AUTODISCOVERY_FLAGS;

    9         options[2].dwOption = NativeMethods.INTERNET_PER_CONN_FLAGS;

   10         options[3].dwOption = NativeMethods.INTERNET_PER_CONN_PROXY_BYPASS;

   11         options[4].dwOption = NativeMethods.INTERNET_PER_CONN_PROXY_SERVER;

   12 

   13         list.dwSize = Marshal.SizeOf(list);

   14         list.pszConnection = null;

   15         list.dwOptionCount = options.Length;

   16         list.dwOptionError = 0;

   17 

   18         int len = Marshal.SizeOf(options[0]);

   19         IntPtr buffer = Marshal.AllocHGlobal((int)(len * options.Length));

   20         for (int i = 0; i < options.Length; i++)

   21         {

   22             Marshal.StructureToPtr(options[i], new IntPtr(buffer.ToInt64() + (i * len)), false);

   23         }

   24         list.pOptions = buffer;

   25         int lpdwBufferLength = Marshal.SizeOf(list);

   26 

   27         NativeMethods.InternetQueryOption(IntPtr.Zero, NativeMethods.INTERNET_OPTION_PER_CONNECTION_OPTION,

   28             list, ref lpdwBufferLength);

   29         for (int i = 0; i < options.Length; i++)

   30         {

   31             options[i] = (NativeMethods.INTERNET_PER_CONN_OPTION)Marshal.PtrToStructure(

   32                 new IntPtr(buffer.ToInt64() + (i * len)), typeof(NativeMethods.INTERNET_PER_CONN_OPTION));

   33         }

   34 

   35         Marshal.PtrToStringAnsi(options[0].ptrValue);

   36         Marshal.PtrToStringAnsi(options[3].ptrValue);

   37         Marshal.PtrToStringAnsi(options[4].ptrValue);

   38 

   39         int flags = options[2].ptrValue.ToInt32();

   40         NativeMethods.GlobalFree(options[0].ptrValue);

   41         NativeMethods.GlobalFree(options[3].ptrValue);

   42         NativeMethods.GlobalFree(options[4].ptrValue);

   43         Marshal.FreeHGlobal(buffer);

   44 

   45         bool enabled = (((flags & NativeMethods.PROXY_TYPE_AUTO_DETECT) > 0) ||

   46             (((flags & NativeMethods.PROXY_TYPE_AUTO_PROXY_URL) > 0) ||

   47             ((flags & NativeMethods.PROXY_TYPE_PROXY) > 0)));

   48 

   49         return enabled;

   50     }

   51 

   52     public static void EnableProxy(bool enable)

   53     {

   54         NativeMethods.INTERNET_PER_CONN_OPTION_LIST list = new NativeMethods.INTERNET_PER_CONN_OPTION_LIST();

   55         NativeMethods.INTERNET_PER_CONN_OPTION[] options = new NativeMethods.INTERNET_PER_CONN_OPTION[1];

   56         options[0].dwOption = NativeMethods.INTERNET_PER_CONN_FLAGS;

   57         options[0].ptrValue = enable ?

   58             new IntPtr(NativeMethods.PROXY_TYPE_AUTO_PROXY_URL |

   59                 NativeMethods.PROXY_TYPE_PROXY |

   60                 NativeMethods.PROXY_TYPE_DIRECT

   61             )

   62             : new IntPtr(NativeMethods.PROXY_TYPE_DIRECT);

   63 

   64         list.dwSize = Marshal.SizeOf(list);

   65         list.pszConnection = null;

   66         list.dwOptionCount = options.Length;

   67         list.dwOptionError = 0;

   68 

   69         int len = Marshal.SizeOf(options[0]);

   70         IntPtr hglobal = Marshal.AllocHGlobal((int)(len * options.Length));

   71         for (int i = 0; i < options.Length; i++)

   72         {

   73             Marshal.StructureToPtr(options[i], new IntPtr(hglobal.ToInt64() + (i * len)), false);

   74         }

   75         list.pOptions = hglobal;

   76 

   77         int dwBufferLength = Marshal.SizeOf(list);

   78         NativeMethods.InternetSetOption(IntPtr.Zero, NativeMethods.INTERNET_OPTION_PER_CONNECTION_OPTION,

   79             list, dwBufferLength);

   80         NativeMethods.InternetSetOption(IntPtr.Zero, NativeMethods.INTERNET_OPTION_REFRESH, null, 0);

   81         Marshal.FreeHGlobal(hglobal);

   82     }

   83 }

This class does the P/Invoke work.

When the application is running, right clicking the tray icon allows toggling from a “proxy enabled” to “proxy disabled” state.

proxy3

When the proxy is enabled and traffic is going through the proxy, the icon shows red monitors.

proxy4

When the proxy is disabled and traffic is not going through the proxy, the icon shows blue monitors.

proxy2

Left-clicking the icon shows the status in a balloon tip.  Clicking the balloon will toggle the proxy status.

proxy1

Note: When enabling the proxy, it only turns on the currently configured proxy.  It will not configure or autoconfigure a proxy.  If there is no proxy configured, I don’t know what will happen.  I haven’t tried it.

Download the full project here or run a ClickOnce application directly here.

Comment on this
Development
| |

Correct Way to Override Object Equality

Wednesday, 12 August 2009 09:24 AM
by Coose

Sometimes different instances of an object can be considered equal.  Take for example a Point class:

class Point

{

    public int X { get; set; }

    public int Y { get; set; }

}

Two different instances of this object that have the same X and Y values should be considered equal objects.  What happens in the following snippet?

Point first = new Point() { X = 1, Y = 2 };

Point second = new Point() { X = 1, Y = 2 };

 

Console.WriteLine("The two points {0} equal", first == second ? "ARE" : "ARE NOT");

You get “The two points ARE NOT equal”.  Additionally:

List<Point> points = new List<Point>();

points.Add(new Point() { X = 1, Y = 2 });

 

Console.WriteLine("The list {0} contain (1,2)",

    points.Contains(new Point() { X = 1, Y = 2 }) ? "DOES" : "DOES NOT");

Gives you “The list DOES NOT contain (1,2)”, even though it really does.

The framework doesn’t know that we intended the two objects to be equal if the X and Y properties are equal.  The framework will consider the objects equal if it is the same object, not if it has the same properties.  But we can surely tell the framework that these are the “same” object if their properties are the same.  It’s pretty easy, but can turn out bad if you don’t do it correctly.

The steps to follow are:

  • Override the GetHashCode method.  This method helps “find” the object based on certain properties.
  • Implement IEquitable<T> interface.  This tells the framework if the objects are actually “equal”.
  • Override the object.Equals method.  This is the non-type specific equivalent of above.
  • Create a == operator, delegates to the object.Equals method.
  • Create a != operator, the inverse of the above.

Let’s break it down:

Override GetHashCode method.

The simplest way to do that is use the exclusive or operator (^) on the GetHashCode of each “unique” property.  So, for our Point class, even if there are other properties or fields, the two that define “uniqueness” of the object are X and Y, so the GetHashCode method becomes:

public override int GetHashCode()

{

    return X.GetHashCode() ^ Y.GetHashCode();

}

Yeah…that’s it.  More unique fields/properties? Just XOR (^) the GetHashCode of them, too.

Implement IEquitable<T>.

There’s only one method: public bool Equals(T other).  This is a three step process:

  1. If the other is null, it’s not equal (obviously this is not null).
  2. If the other is this, it’s obviously equal.
  3. If all “unique” properties are equal, it’s equal.

public bool Equals(Point other)

{

    if (object.ReferenceEquals(other, null)) return false;

    if (object.ReferenceEquals(other, this)) return true;

    return this.X == other.X && this.Y == other.Y;

}

Make sure to use the object.ReferenceEquals to not get into an infinite loop/stack overflow.

Again, other “unique” fields/properties are compared here as well.

Override object.Equals.

This is the non-type specific implementation of the above step, so we compare the types, then call the overload Equals(T) above.

public override bool Equals(object obj)

{

    Point other = obj as Point;

    if (object.ReferenceEquals(other, null)) return false;

    return this.Equals(other);

}

That’s all for that.

Create a == operator.

This is a really simple step, as this operator just calls the object.Equals method.

public static bool operator == (Point x, Point y)

{

    return object.Equals(x, y);

}

Create a != operator.

An even simpler step: just return the inverse of the == operator.

public static bool operator != (Point x, Point y)

{

    return !(x == y);

}

That’s it.  Now the object is compared correctly by the framework.

So, running the original test code:

Point first = new Point() { X = 1, Y = 2 };

Point second = new Point() { X = 1, Y = 2 };

 

Console.WriteLine("The two points {0} equal", first == second ? "ARE" : "ARE NOT");

 

List<Point> points = new List<Point>();

points.Add(new Point() { X = 1, Y = 2 });

 

Console.WriteLine("The list {0} contain (1,2)",

    points.Contains(new Point() { X = 1, Y = 2 }) ? "DOES" : "DOES NOT");

 

Console.ReadLine();

Results in:

The two points ARE equal
The list DOES contain (1,2)

The full Point class is:

class Point : IEquatable<Point>

{

    public int X { get; set; }

    public int Y { get; set; }

 

    public override int GetHashCode()

    {

        return X.GetHashCode() ^ Y.GetHashCode();

    }

 

    public bool Equals(Point other)

    {

        if (object.ReferenceEquals(other, null)) return false;

        if (object.ReferenceEquals(other, this)) return true;

        return this.X == other.X && this.Y == other.Y;

    }

 

    public override bool Equals(object obj)

    {

        Point other = obj as Point;

        if (object.ReferenceEquals(other, null)) return false;

        return this.Equals(other);

    }

 

    public static bool operator == (Point x, Point y)

    {

        return object.Equals(x, y);

    }

 

    public static bool operator != (Point x, Point y)

    {

        return !(x == y);

    }

}

 

And that’s it.

Enjoy.  Or don’t.  Whatever.

Comment on this
Development
|

Send Encrypted Email From C#

Wednesday, 01 July 2009 06:21 PM
by Coose

Email is by-nature not secure.  Network administrators can view mail, and it can easily be seen on the wire.  In order to protect email, there are many methods out there, (PGP, etc).  What I want to address today is encrypting an email message using Asymmetric X.509 Cryptography.  What this means is that the sender will have the public key of the recipient.  This X.509 certificate is used to encrypt the email message body.  All senders will have the same public key.  But, by nature of asymmetric cryptography, only the holder of the private key can decrypt the message.  So, once something is encrypted with a public key, even the sender cannot decrypt the message.  Only the holder of the “other part” of the key (the private key) can decrypt the message.

Now, in order for this to work, a certificate capable of encrypting must be issued.  I won’t address that here.  So, given the subject of an installed X.509 certificate public key, the following class encrypts and sends the message.

(I actually used this class in a WPF sample application, so it is written with INotifyPropertyChanged events for data binding…but that part of the code is not necessary, obviously.)

    1 using System;

    2 using System.Collections.Generic;

    3 using System.Linq;

    4 using System.Web;

    5 using System.ComponentModel;

    6 using System.Security.Cryptography.X509Certificates;

    7 using System.Text;

    8 using System.Security.Cryptography.Pkcs;

    9 using System.IO;

   10 using System.Net.Mail;

   11 

   12 namespace Whatever

   13 {

   14   public class MailMessage : INotifyPropertyChanged

   15   {

   16     private string _to;

   17     private string _from;

   18     private string _subject;

   19     private string _body;

   20     private string _signingCertSubject;

   21     private string _encryptingCertSubject;

   22 

   23     public MailMessage()

   24     {

   25 

   26     }

   27 

   28     #region INotifyPropertyChanged Members

   29 

   30     public event PropertyChangedEventHandler PropertyChanged;

   31 

   32     protected void OnPropertyChanged(string propertyName)

   33     {

   34       if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

   35     }

   36 

   37     #endregion

   38 

   39     public string To

   40     {

   41       get { return _to; }

   42       set { _to = value; OnPropertyChanged("To"); }

   43     }

   44 

   45     public string From

   46     {

   47       get { return _from; }

   48       set { _from = value; OnPropertyChanged("From"); }

   49     }

   50 

   51     public string Subject

   52     {

   53       get { return _subject; }

   54       set { _subject = value; OnPropertyChanged("Subject"); }

   55     }

   56 

   57     public string Body

   58     {

   59       get { return _body; }

   60       set { _body = value; OnPropertyChanged("Body"); }

   61     }

   62 

   63     public string SigningCertSubject

   64     {

   65       get { return _signingCertSubject; }

   66       set { _signingCertSubject = value; OnPropertyChanged("SigningCertSubject"); }

   67     }

   68 

   69     public string EncryptingCertSubject

   70     {

   71       get { return _encryptingCertSubject; }

   72       set { _encryptingCertSubject = value; OnPropertyChanged("EncryptingCertSubject"); }

   73     }

   74 

   75     public void Send()

   76     {

   77       X509Certificate2 signing = null;

   78       X509Certificate2 encrypting = null;

   79 

   80       X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);

   81       store.Open(OpenFlags.ReadOnly);

   82       if (!string.IsNullOrEmpty(this.SigningCertSubject))

   83       {

   84         signing = store

   85           .Certificates

   86           .Find(X509FindType.FindBySubjectName,

   87             this.SigningCertSubject,

   88             false)

   89           .OfType<X509Certificate2>()

   90           .FirstOrDefault();

   91       }

   92       if (!string.IsNullOrEmpty(this.EncryptingCertSubject))

   93       {

   94         encrypting = store

   95           .Certificates

   96           .Find(X509FindType.FindBySubjectName,

   97             this.EncryptingCertSubject,

   98             false)

   99           .OfType<X509Certificate2>()

  100           .FirstOrDefault();

  101       }

  102 

  103       StringBuilder msg = new StringBuilder();

  104       msg.AppendLine("Content-Type: text/plain; charset=\"iso-8859-1\"");

  105       msg.AppendLine("Content-Transfer-Encoding: 7bit");

  106       msg.AppendLine();

  107       msg.AppendLine(this.Body);

  108       byte[] buffer = null;

  109       if (encrypting != null)

  110       {

  111         buffer = Encoding.ASCII.GetBytes(msg.ToString());

  112         EnvelopedCms cms = new EnvelopedCms(new ContentInfo(buffer));

  113         CmsRecipient recip = new CmsRecipient(SubjectIdentifierType.IssuerAndSerialNumber, encrypting);

  114         cms.Encrypt(recip);

  115         buffer = cms.Encode();

  116 

  117         if (signing != null)

  118         {

  119           SignedCms scms = new SignedCms(new ContentInfo(buffer));

  120           CmsSigner signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, signing);

  121 

  122           scms.ComputeSignature();

  123           buffer = scms.Encode();

  124         }

  125       }

  126       store.Close();

  127 

  128       System.Net.Mail.MailMessage mail = new System.Net.Mail.MailMessage();

  129       mail.To.Add(new System.Net.Mail.MailAddress(this.To));

  130       mail.From = new System.Net.Mail.MailAddress(this.From);

  131       mail.Subject = this.Subject;

  132 

  133       if (buffer != null)

  134       {

  135         MemoryStream ms = new MemoryStream(buffer);

  136         AlternateView v = new AlternateView(ms,

  137           "application/pkcs7-mime; smime-type=signed-data;name=smime.p7m");

  138         mail.AlternateViews.Add(v);

  139       }

  140       else

  141       {

  142         mail.Body = this.Body;

  143       }

  144 

  145       SmtpClient client = new SmtpClient();

  146       client.Send(mail);

  147     }

  148   }

  149 }

Now, the recipient of the email (who has the private X.509 key installed) can open this message in Outlook, or other capable email clients.  The private key must be accessible to the email client.

[Todo: insert Outlook screen shots]

Comment on this
Development
|