At any rate, back to the problem. With a multitude of tunnels and potentially SSH servers on the other side of the NAT divide, I wanted a configuration section like this:
1 <TunnelSection>
2 <host SSHServerHostname="tsg" username="user" SSHport="22" password="" privatekey="" privatekeypassphrase="">
3 <tunnels>
4 <tunnel name="tfs" localport="8081" remoteport="8080" destinationserver="tfs.dev.com" />
5 <tunnel name="sql" localport="14331" remoteport="1433" destinationserver="sql2008.dev.com" />
6 <tunnel name="crmapp" localport="81" remoteport="80" destinationserver="crmapp.dev.com" />
7 </tunnels>
8 </host>
9 <host SSHServerHostname="blade16" username="root" SSHport="22" password="" privatekey="" privatekeypassphrase="">
10 <tunnels>
11 <tunnel name="vnc" localport="5902" remoteport="5902" destinationserver="blade1.dev.com" />
12 </tunnels>
13 </host>
14 </TunnelSection>
The example in the microsoft website does not quite do what I wanted to do, although, I suspect that I might have been able to twist it to get it working. At any rate, here is the code:
1 using System; 2 using System.Configuration; 3 4 5 namespace SSHTunnelWF 6 { 7 8 9 public class TunnelSection : ConfigurationSection 10 { 11 12 [ConfigurationProperty("", IsDefaultCollection = true)] 13 public HostCollection Tunnels 14 { 15 get 16 { 17 HostCollection hostCollection = (HostCollection)base[""]; 18 19 return hostCollection; 20 21 } 22 } 23 24 } 25 26 27 public class HostCollection : ConfigurationElementCollection 28 { 29 public HostCollection() 30 { 31 HostConfigElement details = (HostConfigElement)CreateNewElement(); 32 if (details.SSHServerHostname != "") 33 { 34 Add(details); 35 } 36 } 37 38 public override ConfigurationElementCollectionType CollectionType 39 { 40 get 41 { 42 return ConfigurationElementCollectionType.BasicMap; 43 } 44 } 45 46 protected override ConfigurationElement CreateNewElement() 47 { 48 return new HostConfigElement(); 49 } 50 51 protected override Object GetElementKey(ConfigurationElement element) 52 { 53 return ((HostConfigElement)element).SSHServerHostname; 54 } 55 56 public HostConfigElement this[int index] 57 { 58 get 59 { 60 return (HostConfigElement)BaseGet(index); 61 } 62 set 63 { 64 if (BaseGet(index) != null) 65 { 66 BaseRemoveAt(index); 67 } 68 BaseAdd(index, value); 69 } 70 } 71 72 new public HostConfigElement this[string name] 73 { 74 get 75 { 76 return (HostConfigElement)BaseGet(name); 77 } 78 } 79 80 public int IndexOf(HostConfigElement details) 81 { 82 return BaseIndexOf(details); 83 } 84 85 public void Add(HostConfigElement details) 86 { 87 BaseAdd(details); 88 } 89 protected override void BaseAdd(ConfigurationElement element) 90 { 91 BaseAdd(element, false); 92 } 93 94 public void Remove(HostConfigElement details) 95 { 96 if (BaseIndexOf(details) >= 0) 97 BaseRemove(details.SSHServerHostname); 98 } 99 100 public void RemoveAt(int index) 101 { 102 BaseRemoveAt(index); 103 } 104 105 public void Remove(string name) 106 { 107 BaseRemove(name); 108 } 109 110 public void Clear() 111 { 112 BaseClear(); 113 } 114 115 protected override string ElementName 116 { 117 get { return "host"; } 118 } 119 } 120 121 122 public class HostConfigElement:ConfigurationElement 123 { 124 125 [ConfigurationProperty("SSHServerHostname", IsRequired = true, IsKey = true)] 126 [StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")] 127 public string SSHServerHostname 128 { 129 get { return (string)this["SSHServerHostname"]; } 130 set { this["SSHServerHostname"] = value; } 131 } 132 133 [ConfigurationProperty("username", IsRequired = true)] 134 [StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")] 135 public string Username 136 { 137 get { return (string)this["username"]; } 138 set { this["username"] = value; } 139 } 140 [ConfigurationProperty("SSHport", IsRequired = true, DefaultValue = 22)] 141 [IntegerValidator(MinValue = 1, MaxValue = 65536)] 142 public int SSHPort 143 { 144 get { return (int)this["SSHport"]; } 145 set { this["SSHport"] = value; } 146 } 147 148 [ConfigurationProperty("password", IsRequired = false)] 149 public string Password 150 { 151 get { return (string)this["password"]; } 152 set { this["password"] = value; } 153 } 154 155 [ConfigurationProperty("privatekey", IsRequired = false)] 156 public string Privatekey 157 { 158 get { return (string)this["privatekey"]; } 159 set { this["privatekey"] = value; } 160 } 161 162 [ConfigurationProperty("privatekeypassphrase", IsRequired = false)] 163 public string Privatekeypassphrase 164 { 165 get { return (string)this["privatekeypassphrase"]; } 166 set { this["privatekeypassphrase"] = value; } 167 } 168 169 [ConfigurationProperty("tunnels", IsDefaultCollection = false)] 170 public TunnelCollection Tunnels 171 { 172 get { return (TunnelCollection)base["tunnels"]; } 173 174 } 175 176 } 177 178 179 public class TunnelCollection : ConfigurationElementCollection 180 { 181 182 public new TunnelConfigElement this[string name] 183 { 184 get 185 { 186 if (IndexOf(name) < 0) return null; 187 188 return (TunnelConfigElement)BaseGet(name); 189 } 190 } 191 192 public TunnelConfigElement this[int index] 193 { 194 get { return (TunnelConfigElement)BaseGet(index); } 195 } 196 197 public int IndexOf(string name) 198 { 199 name = name.ToLower(); 200 201 for (int idx = 0; idx < base.Count; idx++) 202 { 203 if (this[idx].Name.ToLower() == name) 204 return idx; 205 } 206 return -1; 207 } 208 209 public override ConfigurationElementCollectionType CollectionType 210 { 211 get { return ConfigurationElementCollectionType.BasicMap; } 212 } 213 214 protected override ConfigurationElement CreateNewElement() 215 { 216 return new TunnelConfigElement(); 217 } 218 219 protected override object GetElementKey(ConfigurationElement element) 220 { 221 return ((TunnelConfigElement)element).Name; 222 } 223 224 protected override string ElementName 225 { 226 get { return "tunnel"; } 227 } 228 229 } 230 231 232 public class TunnelConfigElement : ConfigurationElement 233 { 234 235 public TunnelConfigElement() 236 { 237 } 238 239 public TunnelConfigElement(string name, int localport, int remoteport, string destinationserver) 240 { 241 242 this.DestinationServer = destinationserver; 243 this.RemotePort = remoteport; 244 this.LocalPort = localport; 245 this.Name = name; 246 } 247 248 [ConfigurationProperty("name", IsRequired = true, IsKey = true, DefaultValue = "")] 249 public string Name 250 { 251 get { return (string)this["name"]; } 252 set { this["name"] = value; } 253 } 254 255 [ConfigurationProperty("localport", IsRequired = true, DefaultValue =1)] 256 [IntegerValidator(MinValue = 1, MaxValue = 65536)] 257 public int LocalPort 258 { 259 get { return (int)this["localport"]; } 260 set { this["localport"] = value; } 261 } 262 263 [ConfigurationProperty("remoteport", IsRequired = true, DefaultValue =1)] 264 [IntegerValidator(MinValue = 1, MaxValue = 65536)] 265 public int RemotePort 266 { 267 get { return (int)this["remoteport"]; } 268 set { this["remoteport"] = value; } 269 } 270 271 [ConfigurationProperty("destinationserver", IsRequired = true)] 272 [StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")] 273 public string DestinationServer 274 { 275 get { return (string)this["destinationserver"]; } 276 set { this["destinationserver"] = value; } 277 } 278 279 } 280 281 }
The nesting of the collections is achieved by defining a ConfigurationProperty as a class that inherits from the ConfigurationElementCollection class, TunnelCollection in this case, as part of HostConfigElement, which contains all the configuration properties required by the host. This could be repeated ad infinitum by adding a new ConfigurationProperties to the TunnelConfigElement. Make sure that the ElementName of the collection is overridden and matches your config file, in this case host for the HostCollection and tunnel for the TunnelCollection.
Here is the configuration file. Note that the type needs to reference the ConfigSection class, SSHTunnelWF.TunnelSection in this case, and the assembly name SSHTunnelWF:
1 <?xml version="1.0"?>
2 <configuration>
3 <configSections>
4 <section name="TunnelSection" type="SSHTunnelWF.TunnelSection,SSHTunnelWF" />
5 </configSections>
6 <startup>
7 <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
8 </startup>
9 <TunnelSection>
10 <host SSHServerHostname="tsg.edssdn.net" username="user" SSHport="22" password="pass" privatekey="" privatekeypassphrase="">
11 <tunnels>
12 <tunnel name="tfs" localport="8081" remoteport="8080" destinationserver="tfs2010.dev.com" />
13 <tunnel name="sql" localport="14331" remoteport="1433" destinationserver="sql2008.dev.com" />
14 <tunnel name="crm2011app" localport="81" remoteport="80" destinationserver="crm2011betaapp.dev.com" />
15 </tunnels>
16 </host>
17 <host SSHServerHostname="blade16" username="root" SSHport="22" password="pass" privatekey="" privatekeypassphrase="">
18 <tunnels>
19 <tunnel name="vnc" localport="5902" remoteport="5902" destinationserver="blade1.dev.com" />
20 </tunnels>
21 </host>
22 </TunnelSection>
23 </configuration>
Finally, you can access the contents of this custom configuration section like this:
TunnelSection tunnels = ConfigurationManager.GetSection("TunnelSection") as TunnelSection