Anda di halaman 1dari 16

ASP.NET/ IIS applications use an optional XML-based configuration file named web.

config, to
maintain application configuration settings. This extends and/ or changes any settings in the system
wide configuration file, machine.config.

You can, in fact, have one web.config file per directory within your IIS application each possibly
overriding or extending the cumulative settings of the files of the levels above back to the
machine.config file. Note however that the power of the web.config in sub-directories is more limited
than those in the application root, as we shall see.

These files allow us to configure our application without resorting to the IIS MMC snap-in, as we
previously had to with classic ASP, thus reducing the need for the involvement of the web site
administrator to get your application working as you desire. You may simply copy your web.config
file(s) to the appropriate location(s) within your application file system. Further, changes to
web.config files are automatically detected so there is no requirement to restart the web server, or
unload/ reload the application for example, both of which would require administrative access rights
to the server. Note that there is one exception to this, which we shall mention below.

As already stated the web.config files initially inherit from the machine wide machine.config file
located at

c:\winnt\Microsoft.NET\Framework\[version]\CONFIG\machine.config

Unless you override the settings in your application’s web.config files it is these settings that will
pervade. Let’s first take a look at this file before looking at web.config in detail.

Machine.Config

We'll cover web.config briefly before focusing on the file you’ll more commonly change in a
production server environment: web.config.

One of the primary purposes of machine.config is to define the configuration elements available to us
in web.config files. These are defined under the <configSections> element. Each entry defines the
type of the section. For example the top of your machine.config file will look something like:

<?xml version="1.0" encoding="UTF-8"?>


<configuration>

<configSections>
.
.
.
</configSections>
.
.
.

In particular if you look for the sub-entries under the system.web section you'll see the entries we're
most interested in as ASP.NET developers. You'll see entries for globalization, authorization and
sessionState, amongst several others, and we'll return to how we might use these in the web.config
file in the next section. For example,
<section name="globalization"
type="System.Web.Configuration.GlobalizationConfigurationHandler, System.Web,
Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>

Specifies and registers a section name, which we can then reference in our config files, that defines
the type of the handler for section, amongst other attributes.

Let's list summary contents of the system.web section now so we can see what kind of functionality
we have access to in these configuration files. Also noted are any limitations regarding when and
where the elements can be used – as introduced earlier some elements are not valid at the
application sub-directory level. If not obvious from the section name I shall provide a short
description of the nature of the setting. If anything is unfamiliar and not explained don't worry, as
we'll be looking at most of the options in more detail very shortly.

Section Description (where applicable)


authentication
authorization
browserCaps i.e. Browser Capabilities
compilation
customErrors
globalization
httpHandlers
httpModules
httpRuntime
Identity
machineKey not configurable at the sub-directory level
Pages
processModel only configurable within machine.config
securityPolicy not configurable at the sub-directory level
sessionState
Trace
webServices

In the above, the section names typically represent the namespace that the settings apply to.

Also important but defined outside of the system.web group is

<section name="appSettings" … which allows the definition of your own application settings as per
the example in the machine.config file:

<appSettings>
<add key="XML File Name" value="myXmlFileName.xml" />
</appSettings>
This allows you to store name/ value pairs that are easily accessible to your application pages, as we
shall see shortly. You might store your database connection strings via this facility, for example. As
an aside, it is not recommended that you store connection strings in application objects in ASP.NET,
as would have been common practice in classic ASP as the new (and vastly improved) debugging and
tracing facilities make this option less secure.

Web.Config

As an ASP.NET developer this is the type of configuration file you will be concerned with the vast
majority of the time. It is an XML document, as per machine.config, with sections for different
settings grouped together according to a tag under a <configuration> element. The core ASP.NET
configuration settings are nested together inside the <system.web></system.web> tags.

In this section we're going to look at the different elements listed in the last section in a little more
detail. The examination won’t be exhaustive, as many of the sections deserve articles in their own
right. The focus shall be on listing the options available to the developer and providing an overview
of the main elements and how you might employ them in your applications. The aim is to give you an
appreciation of the range of functionality you have at your disposal, for your further exploration.

appSettings

Configures custom application settings you can then access in your applications. It has three child
elements: add, remove and clear. E.g.:

<appSettings>
<add key="ConnString" value=" server=localhost; database=test; uid=testing;
pwd=test;" />
</appSettings>

The setting is then accessible from our application pages via:

ConfigurationSettings.AppSettings(“ConnString”)

Remove is used to remove particular settings; clear (as follows) is used to remove all appSettings
that were defined at a higher level in the configuration file hierarchy:

<appSettings>
<clear/>
</appSettings>

Authentication and Authorization

Authentication and authorization are closely related topics and concerned with the security of your
application.

Authentication defines the authentication method your application uses and parameters thereof;
authorization controls client access to URL resources and works in tandem with Access Control Lists
(ACLs) which define directory and file level authorization.
<authentication mode="Windows|Forms|Passport|None">
<forms name="name"
loginUrl="url"
protection="All|None|Encryption|Validation"
timeout="30" path="/" >
<credentials passwordFormat="Clear|SHA1|MD5">
<user name="username" password="password" />
</credentials>
</forms>
<passport redirectUrl="internal"/>
</authentication>

The mode attribute defines the authority against which .NET authenticates. These may be

• Integrated windows authentication using NTLM or Kerberos.


• forms (cookie) based authentication
• passport authentication

or none. If forms based authentication is chosen your authority may be specified to be the
credentials sub-section of the authentication section where passwords can be presented as cleartext
or encrypted using hashing algorithms of varying security. Also configurable for forms based
authentication are the name of the cookie that shall be used to maintain authentication between
browser requests, the login URL for unauthorised requests, and the level of protection, amongst
other attributes.

To authorization:

<authorization>
<allow users="comma-separated list of users"
roles="comma-separated list of roles"
verbs="comma-separated list of verbs" />

<deny users="comma-separated list of users"


roles="comma-separated list of roles"
verbs="comma-separated list of verbs" />
</authorization>

ASP.NET thus allows refinement over ACL based authorization based on the URL requested as well as
the HTTP request method attempted via the verb attribute, valid values of which are: GET, POST,
HEAD or DEBUG. Thus typically you might deny access to the anonymous user account, indicated by
‘?’, as follows in a complete example:

<configuration>
<system.web>
<authentication mode="Forms">
<forms name=".AUTHCOOKIE" loginURL="login.aspx" protection="All" />
</authentication>
<machineKey validationKey="Autogenerate" decryption key="Autogenerate"
validation"SHA1" />
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>

Thus in this example if a user had not logged into the application there would be denied access to
any resource in the application and redirected to login.aspx and when successfully logged in would
then be able to access any of the resources in the application, conditional on any overriding settings
in any application sub-directories. This access level would be retained for the duration of their
session, unless otherwise defined in login.aspx.

For more detail on the above and related issues you may also be interested in my article on
developing a secure login facility on www.dotnetjohn.com

BrowserCaps

Controls the settings of the browser capabilities component.

<browserCaps>
<result type="class" />
<use var="HTTP_USER_AGENT" />
browser=Unknown
version=0.0
majorver=0
minorver=0
frames=false
tables=false
<filter>
<case match="Windows 98|Win98">
platform=Win98
</case>
<case match="Windows NT|WinNT">
platform=WinNT
</case>
</filter>
<filter match="Unknown" with="%(browser)">
<filter match="Win95" with="%(platform)">
</filter>
</filter>
</browserCaps>

This is one of the aforementioned topics that justify an article in itself. This allows configuration of
settings of an object instantiated from the HttpBrowserCapabilitiesClass used by the application.
Amongst other capabilities this class controls the rendering of HTML and code by ASP.NET. It also
allows determination of the client browser’s capabilities within an ASP.NET web form. This class
exposes the ClientTarget property, amongst other properties and methods, which control the output
rendered by an ASP.NET web form and the controls therein. These values are what you are
effectively setting via the browsercaps config element.

Microsoft has enabled control of the rendered output providing the options of output as HTML3.2 or
HTML4.0 with Javascript. The decision on which option to use is based on the capabilities of the client
browser in the areas of HTML, DHTML and CSS support though the HttpBrowserCapabilities Class
allows much more granular detection of the client browser capabilities should you require this level of
control. These may also be options provided by the client browser but which the user has chosen to
disable. Attributes include whether support exists for cookies, frames, tables, VBScript, etc. as well
as information regarding the platform and OS the client browser is running on.

Here's an example, as per the SDK documentation, that demonstrates parsing the User-Agent HTTP
header (as specified by the use element) for any version of Internet Explorer. This example makes
use of regular expressions to capture and transfer information from the User Agent string to
properties of browserCaps.

<configuration>
<browserCaps>
<result type="System.Web.HttpBrowserCapabilities, System.Web" />
<use var="HTTP_USER_AGENT" />
browser=Unknown
version=0.0
majorversion=0
minorversion=0
frames=false
tables=false
cookies=false
backgroundsounds=false
<filter>
<case match="^Mozilla[^(]*\(compatible; MSIE
(?'ver'(?'major'\d+)(?'minor'\.\d+)(?'letters'\w*))
(?'extra'.*)">
browser=IE
version=${ver}
majorver=${major}
minorver=${minor}
<case match="^2\." with="%{version}">
tables=true
cookies=true
backgroundsounds=true
<case match="2\.5b" with="%{version}">
beta=true
</case>
</case>
</case>
</filter>
</browsercaps>
</configuration>

For the full <browserCaps> section, with a more complete example of this syntax, see the
Machine.config file that is installed with the .NET Framework. Unless you have quite specific and
unusual application needs you’ll probably not have great call to further configure the <browserCaps>
section over and above what Microsoft has already done for you.

Compilation

Configures compilation settings for use when the .NET framework dynamically compiles resources.
<compilation debug="true|false"
batch="true|false"
batchTimeout="number of seconds"
defaultLanguage="language"
explicit="true|false"
maxBatchSize="maximim number of pages per
batched compilation"
maxBatchGeneratedFileSize="maximum combined size (in KB)
of the generated source file per
batched compilation"
numRecompilesBeforeAppRestart="number"
strict="true|false"
tempDirectory="directory under which the ASP.NET temporary
files are created" >
<compilers>
<compiler language="language"
extension="ext"
type=".NET Type"
warningLevel="number"
compilerOptions="options" />
</compilers>

<assemblies>
<add assembly="assembly" />
<remove assembly="assembly" />
<clear />
</assemblies>
</compilation>

We shan't enter into a great level of detail regarding this section, as this is one of the sections the
developer is unlikely to re-configure substantially. Take a look at your machine.config file for an
example. Typically, you’ll have 3 compilers setup for VB, C# and Javascript as well as several
assemblies, e.g. system.web, system.data, system.web.services, system.xml, etc. that are
commonly used during compilation of an ASP.NET resource.

Picking out one option of note: the debug attribute specifies whether the framework should compile
debug binaries or retail binaries – in a production environment when debugging is complete, debug
should be set to false to increase performance.

CustomErrors

Configures settings for handling web application errors, as follows:

<customErrors defaultRedirect="url" mode="On|Off|RemoteOnly">


<error statusCode="statuscode" redirect="url"/>
</customErrors>

The mode attribute specifies how custom error handling is handled with possibly values being:

on: enabled for all requests


off: not enabled
RemoteOnly: enabled only for remote clients; requests from the local machine are not handled by
custom error settings.

Within <customErrors> you may specify how particular error codes are handled via the redirect
attribute of an <error> child element. e.g. 404 errors should be directed to a ‘File not found’ page.
For example:

<customErrors defaultRedirect="genericerror.htm" mode="RemoteOnly">


<error statusCode="500" redirect="InternalError.htm"/>
</customErrors>

Globalization

Globalization is primarily concerned with localization issues. Localization is the customization of data
and resources for specific 'locales' or languages. A locale categorizes a collection of data and rules
specific to a language and geographical area. These include information on sorting rules, date and
time formatting, numeric and monetary conventions and symbols, and character encoding.

ASP.NET and it’s underlying technologies use Unicode, which makes our life easier due to the
extensive representational capabilities of this standard. The use of Unicode with culture encodings
allows us to tailor response data, as follows:

<globalization requestEncoding="any valid encoding string"


responseEncoding="any valid encoding string"
fileEncoding="any valid encoding string"
culture="any valid culture string"
uiCulture="any valid culture string" />

Request and response encoding specify the assumed encoding requests and responses. The default
specified in machine.config is UTF-8 (a particular Unicode standard). If not specified they default to
the host systems regional options locale setting.

Culture specifies the default culture for processing incoming Web requests and specifies the
CultureInfo class to be used. The CultureInfo class holds culture-specific information, such as the
associated language, sub-language, country/region, calendar, and cultural conventions. An example
value is: 'en-GB'

UiCulture specifies the default culture for processing locale-dependent resource searches. Again this
relates to the application CultureInfo information.

HttpHandlers, HttpModules and HttpRuntime

These three sections are interlinked and come under the overall topic of the Http runtime. The
ASP.NET Http runtime allows you to extend the functionality of ASP.NET. This is another example of
the increased power of ASP.NET over classic ASP. If ASP.NET does not provide sufficient functionality
in a chosen area, the developer can alter the underlying application infrastructure to enable this
functionality. This form of activity would previously been external to the work boundaries of the web
developer – it would have been done in C or C++ using the Internet Services Application
Programming Interface (ISAPI). Now it can be done with any .NET languages with the support for the
framework.
The Http runtime is customizable in your configuration file(s) via the following syntax:

<httpRuntime useFullyQualifiedRedirectUrl="true|false"
maxRequestLength="size in kbytes"
executionTimeout="seconds"
minFreeThreads="number of threads"
minFreeLocalRequestFreeThreads="number of threads"
appRequestQueueLimit="number of requests" />

E.g.:

<httpRuntime maxRequestLength="4000"
useFullyQualifiedRedirectUrl="true"
executionTimeout="45"/>

An Http module is an assembly that implements the IhttpModule interface and handles events.
ASP.NET includes a set of HTTP modules that can be used by your application. For example, the
SessionStateModule is provided by ASP.NET to supply session state services to an application.
Custom HTTP modules can be created to respond to either ASP.NET events or user events and can be
registered with your application via web.config using the following syntax:

<httpModules>
<add type="classname, assemblyname" name="modulename" />
<remove name="modulename" />
<clear />
</httpModules>

Http handlers map incoming requests to the appropriate IHttpHandler or IHttpHandlerFactory class,
according to the URL and HTTP verb specified in the request, as follows:

<httpHandlers>
<add verb="verb list" path="path/wildcard" type="type,assemblyname" validate="" />
<remove verb="verb list" path="path/wildcard" />
<clear />
</httpHandlers>

Identity

Controls the application identity of the Web application.

<identity impersonate="true|false" userName="username" password="password"/>

Impersonation is the concept whereby an application executes under the context of the identity of
the client that is accessing the application. This is achieved by using the access token provided by
IIS. You may well know that by default the ASPNET account is used to access ASP.NET resources via
the Aspnet_wp.exe process. This, by necessity, has a little more power than the standard guest
account for Internet access, IUSR, but not much more. Sometimes you may wish to use a more
powerful account to access system resources that your application needs. This may be achieved via
impersonation as follows:

<system.web>
<identity impersonate=”true” />
</system.web>

or you may specify a particular account:

<system.web>
<identity impersonate=”false” userName=”domain\sullyc” password=”password” />
</system.web>

Of course you will need to provide the involved accounts with the necessary access rights to achieve
the goals of the application. Note also that if you don't remove IUSR from the ACLs then this is the
account that will be used – this is unlikely to meet your needs as this is a less powerful account that
ASPNET.

MachineKey

Configures keys to use for encryption and decryption of forms authentication cookie data and hence
is used in conjunction with authentication and authorization elements.

<machineKey validationKey="autogenerate|value"
decryptionKey="autogenerate|value"
validation="SHA1|MD5|3DES" />

SHA1, MD5 and 3DES are encryption algorithms of various levels of security.

Pages

Defines page-specific configuration settings for all application pages. These may be overridden at the
page level.

<pages buffer="true|false"
enableSessionState="true|false|ReadOnly"
enableViewState="true|false"
enableViewStateMac="true|false"
autoEventWireup="true|false"
smartNavigation="true|false"
pageBaseType="typename, assembly"
userControlBaseType="typename" />

Explaining these:

buffer specifies whether the URL resource uses response buffering, i.e. whether the page is sent in
one go or in sections as available.
enableSessionState specifies whether session state is enabled.

enableViewState specifies whether viewstate is enabled.

enableViewStateMac is actually undocumented in the .NET SDK but the corresponding page property
is not intended to be used directly from your code.

autoEventWireup indicates whether page events are automatically enabled.

smartNavigation indicates whether smart navigation is enabled. When a page is requested by an


Internet Explorer 5 browser, or later, smart navigation enhances the user's experience of the page by
performing the following:

• eliminating the flash caused by navigation.


• persisting the scroll position when moving from page to page.
• persisting element focus between navigations.
• retaining only the last page state in the browser's history.

Smart navigation is best used with ASP.NET pages that require frequent postbacks but with visual
content that does not change dramatically on return.

pageBaseType specifies a code-behind class that .aspx pages inherit by default.

userControlBaseType specifies a code-behind class that user controls inherit by default.

ProcessModel

Configures the ASP.NET process model settings on an Internet Information Services (IIS) Web server,
controlling the spawning and killing of ASP.NET worker processes.

<processModel enable="true|false"
timeout="mins"
idleTimeout="mins"
shutdownTimeout="hrs:mins:secs"
requestLimit="num"
requestQueueLimit="Infinite|num"
restartQueueLimit="Infinite|num"
memoryLimit="percent"
cpuMask="num"
webGarden="true|false"
userName="username"
password="password"
logLevel="All|None|Errors"
clientConnectedCheck="HH:MM:SS"
comAuthenticationLevel="Default|None|Connect|Call|
Pkt|PktIntegrity|PktPrivacy"
comImpersonationLevel="Default|Anonymous|Identify|
Impersonate|Delegate"
maxWorkerThreads="num"
maxIoThreads="num" />
by default the specification in machine.config is, with notes,

<processModel enable="true"

Process will not automatically restart at periodic time


timeout="Infinite"
intervals

idleTimeout="Infinite" Or after a set idle time

If the process does not gracefully shutdown in 5


shutdownTimeout="0:00:05"
seconds it will be killed

Specifies the number of requests allowed before


requestLimit="Infinite" ASP.NET automatically launches a new worker process
to take the place of the current one.

Process will restart after the request queue has


requestQueueLimit="5000"
reached 5000, thus preventing deadlocks.

restartQueueLimit="10"

Restart the process if ASP.NET uses up 60% of the


memoryLimit="60"
available memory.

webGarden="false" A multiprocessor Web server is called a Web garden.

Specifies which processors on a multiprocessor server


cpuMask="0xffffffff" are eligible to run ASP.NET processes, if webGarden is
set to false. If true CPU usage is scheduled by the OS.

Machine, when used in conjunction with AutoGenerate


userName="machine" as the password value, runs the process as an
unprivileged ASP.NET service account.

password="AutoGenerate"

Specifies that only unexpected shutdowns, memory


logLevel="Errors" limit shutdowns, and deadlock shutdowns are logged.
Errors is the default.

The request is left in the queue for 5 seconds before


clientConnectedCheck="0:00:05"
ASP.NET does a client connected check.

Specifies that DCOM authenticates the credentials of


comAuthenticationLevel="Connect" the client only when the client establishes a
relationship with the server.

Specifies that the server process can impersonate the


comImpersonationLevel="Impersonate" client's security context while acting on behalf of the
client.

9 minutes must elapse after the last restart to cure a


responseRestartDeadlockInterval="00:09:00" deadlock before the process is restarted to cure a
deadlock again.

responseDeadlockInterval="00:03:00" The process will be restarted after 4 minutes if the


following conditions are met: · There are queued
requests. · There has not been a response during this
interval.

Up to 25 threads are to be used for the process on a


maxWorkerThreads="25"
per-CPU basis.

maxIoThreads="25"/>

Notes:

The managed code configuration system does not read the <processModel> configuration settings.
Instead, they are read directly by the aspnet_isapi.dll unmanaged DLL. Changes to this section are
not applied until IIS is restarted.

When ASP.NET is running under IIS version 6 in native mode, the IIS 6 process model is used and
the settings in the <processModel> section are ignored. To configure the process identity, cycling, or
other process model values, use the Internet Services Manager UI to configure the IIS worker
process for your application.

securityPolicy

The .Net Framework security system is governed by a configurable set of rules collectively defining
the system security policy. This policy allows the end user or administrator to adjust the settings that
determine which resources code is allowed to access and ultimately decide which code is allowed to
run at all.

The securityPolicy section defines valid mappings of named security levels to policy files. You can
extend the security system by providing your own named <trustLevel> sub-tag mapped to a file
specified by the policyFile attribute.

<securityPolicy>
<trustLevel name="value" policyFile="value" />
</securityPolicy>

If you look in your c:\winnt\Microsoft.NET\Framewrok\[version]\CONFIG\ directory you will find a


variety of .config files. For example, in web_hightrust.config processModel uses the system account –
a more powerful account than the standard machine.config. In machine.config itself the following
exists:

<securityPolicy>
<trustLevel name="Full" policyFile="internal"/>
<trustLevel name="High" policyFile="web_hightrust.config"/>
<trustLevel name="Low" policyFile="web_lowtrust.config"/>
<trustLevel name="None" policyFile="web_notrust.config"/>
</securityPolicy>

sessionState

Configures session state settings for the current application.


<sessionState mode="Off|Inproc|StateServer|SQLServer"
cookieless="true|false"
timeout="number of minutes"
stateConnectionString="tcpip=server:port"
sqlConnectionString="sql connection string" />

Mode specifies where to store the session state information between browser requests whether
Inproc (locally), StateServer (remote server) or on SQLServer. If StateServer is selected the
StateConnectionString must be specified. If SQLServer, the SQLServer Connection string must
accordingly be specified. You may also specify whether cookies should be used to maintain state and
the duration of user inactivity before their session is abandoned and any corresponding state
information is discarded.

Trace

Configures the ASP.NET trace service for the application. These settings can be overridden within the
Page directive of the individual pages.

<trace enabled="true|false"
localOnly="true|false"
pageOutput="true|false"
requestLimit="integer"
traceMode="SortByTime|sortByCategory" />

These are fairly self-explanatory. Setting localOnly to true makes the trace viewer (trace.axd)
available only on the host Web server. The requestLimit is the number of trace requests to store on
the server. The default is 10.

Once you have enabled tracing for your application, when each page in the application is requested,
it will execute any trace statements that it contains. You can view these statements and the
additional trace information in the trace viewer. You can view the trace viewer by requesting
trace.axd from the root of your application directory.

webServices

Controls the settings of XML Web services created using ASP.NET, as follows:

<webServices>
<protocols>
<add name="protocol name" />
</protocols>
<serviceDescriptionFormatExtensionTypes>
</serviceDescriptionFormatExtensionTypes>
<soapExtensionTypes>
<add type="type" />
</soapExtensionTypes>
<soapExtensionReflectorTypes>
<add type="type" />
</soapExtensionReflectorTypes>
<soapExtensionImporterTypes>
<add type="type" />
</soapExtensionImporterTypes>
<wsdlHelpGenerator href="help generator file"/>
</webServices>

The default configuration in machine.config is:

<webServices>
<protocols>
<add name="HttpSoap"/>
<add name="HttpPost"/>
<add name="HttpGet"/>
<add name="Documentation"/>
</protocols>
<soapExtensionTypes>
</soapExtensionTypes>
<soapExtensionReflectorTypes>
</soapExtensionReflectorTypes>
<soapExtensionImporterTypes>
</soapExtensionImporterTypes>
<wsdlHelpGenerator href="DefaultWsdlHelpGenerator.aspx"/>
<serviceDescriptionFormatExtensionTypes>
</serviceDescriptionFormatExtensionTypes>
</webServices>

Again, further detail is outside the scope of this article.

Adding custom configuration settings

As well as being able to define our own custom application settings within the <appSettings> section
of the web.config file, as introduced above, we can add new custom configuration sections simply by
adding a <configSections> element, following a similar pattern to the definitions of other elements in
machine.config, as follows:

<configuration>
<configSections>
<section name="mySettings"
type="System.Configuration.NameValueSectionHandler,System" />
</configSections>

<mySettings>
<add key="ConnString" value="server=localhost; database=test; uid=testing;"/>
</mySettings>
</configuration>

Accessing these values is similar to using AppSettings:

ConfigurationSettings.GetConfig("mySettings")("ConnString")
Conclusion

Hopefully the above has provided a useful overview of the options open to you as an ASP.NET
developer to further configure your applications as well as providing some insight into what .NET is
doing behind the scenes to support your application. Due to the breadth of the subject matter only
an overview was possible within the text of this article. However, please explore links within the
above to other articles on this site as well as keeping an eye out for future related articles.

Anda mungkin juga menyukai