We are using the excellent ELMAH to deal with unhandled exceptions in an ASP.NET 3.5 web application. This works extremely well for all of the site apart from WCF services which are being consumed using the REST features. When an exception occurs within the operation methods that is not handled by the application code, WCF handles it in various ways depending on the service contracts and configuration settings. This means that the exception does not end up firing the ASP.NET HttpApplication.Error event that ELMAH uses. The two solutions I am aware of to deal with this are:
Wrap all method calls in a try { } catch(Exception ex) { Elmah.ErrorSignal.FromCurrentContext().Raise(ex); throw; } to explicitly call Elmah within the catch block.
Use IErrorHandler as described in Will Hughes' blog post Making WCF and ELMAH play nice together to factor out the call to ELMAH to a separate ErrorHandler.
The first option is extremely simple but is not exactly DRY. The second option only requires you to decorate each service with the custom attribute after implementing the attribute and the ErrorHandler. I have done this based on Will's work but I want to verify that this is the correct approach before posting the code.
Is there a better way that I have missed?
The MSDN documenation for IErrorHandler says that the HandleError method is the place to do the logging but ELMAH accesses the HttpContext.Current.ApplicationInstance, which is null within this method even though HttpContext.Current is available. Making the call to Elmah within the ProvideFault method is a workaround as ApplicationInstance is set but this does not match the intent described in the API documentation. Am I missing something here? The documentation does state that you should not rely on the HandleError method being called on the operation thread which may be why ApplicationInstance is null in this scope.
The solution from my blog post (referenced in the OP) was based on an existing solution we were/are using to alter HTTP Response Codes during an error state.
So, for us it was a one-line change to pass the Exception to ELMAH. If there's a better solution, I'd love to know about it too.
For Posterity/Reference, and potential improvement - here's the code from the current solution.
HttpErrorHandler and ServiceErrorBehaviourAttribute Classes
using System;
using System.ServiceModel;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Collections.ObjectModel;
using System.Net;
using System.Web;
using Elmah;
namespace YourApplication
{
/// <summary>
/// Your handler to actually tell ELMAH about the problem.
/// </summary>
public class HttpErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
return false;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error != null ) // Notify ELMAH of the exception.
{
if (System.Web.HttpContext.Current == null)
return;
Elmah.ErrorSignal.FromCurrentContext().Raise(error);
}
}
}
/// <summary>
/// So we can decorate Services with the [ServiceErrorBehaviour(typeof(HttpErrorHandler))]
/// ...and errors reported to ELMAH
/// </summary>
public class ServiceErrorBehaviourAttribute : Attribute, IServiceBehavior
{
Type errorHandlerType;
public ServiceErrorBehaviourAttribute(Type errorHandlerType)
{
this.errorHandlerType = errorHandlerType;
}
public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase)
{
}
public void AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
{
IErrorHandler errorHandler;
errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType);
foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher;
channelDispatcher.ErrorHandlers.Add(errorHandler);
}
}
}
}
Usage Example
Decorate your WCF Services with the ServiceErrorBehaviour Attribute:
[ServiceContract(Namespace = "http://example.com/api/v1.0/")]
[ServiceErrorBehaviour(typeof(HttpErrorHandler))]
public class MyServiceService
{
// ...
}
When creating a BehaviorExtensionElement it is even possible to activate the behavior using config:
public class ErrorBehaviorExtensionElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(ServiceErrorBehaviourAttribute); }
}
protected override object CreateBehavior()
{
return new ServiceErrorBehaviourAttribute(typeof(HttpErrorHandler));
}
}
Config:
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="elmah" type="Namespace.ErrorBehaviorExtensionElement, YourAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
<behaviors>
<serviceBehaviors>
<behavior>
<elmah />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
That way it is also possible to use ELMAH in combination with RIA services!
This may well be obvious to some people but I just spent quite a while trying to figure out why my HttpContext.Current was null despite following all of Will Hughes' excellent answer. Embarassingly, I realised that this was because my WCF service is activated by a MSMQ message.
I ended up rewriting the ProvideFault() method:
if (HttpContext.Current == null)
{
ErrorLog.GetDefault(null).Log(new Error(error));
}
else
{
ErrorSignal.FromCurrentContext().Raise(error);
}
I have done this based on Will's work
but I want to verify that this is the
correct approach before posting the
code.
I think this is a great approach (kudos to Will for this posting!). I don't think Will or you have missed anything here. Implementing IErrorHandler is the preferred way of capturing all possible server-side exceptions that could otherwise cause the communication channel to be faulted (torn down) and thus it's a natural place to hook in some logging like ELMAH.
Marc
I was unable to get the proposed answer working with a WCF Data Service. I wired up the behavior attribute, etc, but still did not get any errors logged. Instead, I ended up adding the following to the service implementation:
protected override void HandleException(HandleExceptionArgs args)
{
Elmah.ErrorSignal.FromCurrentContext().Raise(args.Exception);
base.HandleException(args);
}
I haven't tried doing this explicitly with the REST stuff, and haven't used ELMAH myself, but another option worth looking into might be to hook into WCF using an IDispatchMessageInspector instead of an IErrorHandler.
Related
I am using AmazonIapV2Android.dll provided by Amazon team for the Xamarin.Android project. I have implemented it last year and have been using successfully with Dx+proguard with using proguard rules as below. Those lines are also suggested by Amazon documentation. see the link
-dontwarn com.amazon.**
-keep class com.amazon.** {*;}
-keepattributes *Annotation*
Recently I have changed my xamarin.android project using d8+r8 using the same proguard file. Everything, google iap implementation also fine but Amazon IAP started throwing exception.
Jsonable.CheckForErrors
(System.Collections.Generic.Dictionary`2[TKey,TValue] jsonMap)
com.amazon.device.iap.cpt.AmazonException: java.lang.RuntimeException:
Missing type parameter.
at com.amazon.device.iap.cpt.RequestOutput.CreateFromJson
(System.String jsonMessage) [0x0002d] in
<26520843ea114e5a91256077e0412906>:0 \n at
com.amazon.device.iap.cpt.AmazonIapV2Impl+AmazonIapV2Base.GetProductData
(com.amazon.device.iap.cpt.SkusInput skusInput) [0x00013] in
I am using also linker as User and sdk assemblies, this is triggering obfuscation obviously and some methods are removed by the linker because using Sdk assemblies only or No Linking, everything works fine.
I have added the AmazonIapV2Android as linker to skip but it didnt help.
When I check the code implementation of the RequestOutput.CreateFromJson function implementation, it looks like as below.
using com.amazon.device.iap.cpt.json;
namespace com.amazon.device.iap.cpt
{
public sealed class RequestOutput : Jsonable
{
public string RequestId{get;set;}
public static RequestOutput CreateFromJson(string jsonMessage)
{
try
{
Dictionary<string, object> jsonMap = Json.Deserialize(jsonMessage) as Dictionary<string, object>;
Jsonable.CheckForErrors(jsonMap);
return CreateFromDictionary(jsonMap);
}
catch(System.ApplicationException ex)
{
throw new AmazonException("Error encountered while UnJsoning", ex);
}
}
and implementation for Jsonable in the dll looks as below
namespace com.amazon.device.iap.cpt
{
public abstract class Jsonable
{
public static Dictionary<string, object> unrollObjectIntoMap<T>(Dictionary<string, T> obj) where T:Jsonable
{
Dictionary<string, object> jsonableDict = new Dictionary<string, object>();
foreach (var entry in obj)
{
jsonableDict.Add (entry.Key, ((Jsonable)entry.Value).GetObjectDictionary());
}
return jsonableDict;
}
public static List<object> unrollObjectIntoList<T>(List<T> obj) where T:Jsonable
{
List<object> jsonableList = new List<object>();
foreach (Jsonable entry in obj)
{
jsonableList.Add(entry.GetObjectDictionary());
}
return jsonableList;
}
public abstract Dictionary<string, object> GetObjectDictionary();
public static void CheckForErrors(Dictionary<string, object> jsonMap)
{
object error;
if (jsonMap.TryGetValue("error", out error))
{
throw new AmazonException(error as string);
}
}
}
}
I have tried to use linker.xml with settings like below also but it didnt help either.
<assembly fullname="AmazonIapV2Android">
<namespace fullname="com.amazon.device.iap.cpt" />
<namespace fullname="com.amazon.device.iap.cpt.log" />
<namespace fullname="com.amazon.device.iap.cpt.json" />
</assembly>
I cannot figure out why should throw exception while i am defining keepclass for all methods and members under the namespace starting with com.amazon prefix.
Any idea what could be the reason here?
EDIT: just had several more tests and my initiale comment was slightly wrong. strange way app is working in debug with Linker set "SDK assemblies only" but in release it doesnt work even with "SDK assemblies only"
Obviously this is a known problem for using R8 and Amazon IAP. Typical amazon doesnt care and update their package. especially there is no update for Xamarin IAP since 2016.
Here are the links to problem
https://forums.developer.amazon.com/questions/205480/in-app-billing-not-working-since-android-studio-de.html
https://issuetracker.google.com/issues/134766810
Currently there are 3 workarounds,
disable r8. Bad is that no obfuscation, no optimization.
Use dx+proguard+multi dex instead of d8+r8. There is a problem here if you use androidx, androidx libraries dont work with dx+proguard, they work only with d8+r8, you need to go back to support libraries.
I am not sure but amazon website claims that it is claimed, it works with r8 but this is pobably for the android java library not for xamarin. Because as i cheked there is newer version to as jar. You can theoretically use Binding library to get a new dll and try but I read even for Android studio projects, this doesnt work. So i tried to create a binding library and it had many errors and api seems to be different than xamarin. It is a lot of effort for non-profitable app store.
here is the link to github issue on xamarin.android as well.
I have a Spring Boot app with a few Controllers I want to track their dependencies (including outbound Http requests). That all works as expected. However, I have one controller for a health check (returning 204) that I do not want telemetry for. All other responses mention custom code components, but according to the documentation, this should be doable within the AI-Agent.xml config.
<BuiltInProcessors>
<Processor type="RequestTelemetryFilter">
<Add name="NotNeededResponseCodes" value="204" />
</Processor>
</BuiltInProcessors>
I notice on the classpath that there are two RequestTelemtryFilter instances (one from ai-core and one from ai-web, neither of which get hit when i debug).
Configuring the Agent (via AI-Agent.xml) is different than configuring custom telemetry (via Applicationinsights.xml). Spring boot + the agent requires the use of a custom Telemetry Processor and pulling into your configuration via #Bean. No additional XML in the AI-Agent is necessary.
public class HealthCheckTelemetryFilter implements TelemetryProcessor
{
public HealthCheckTelemetryFilter()
{
// TODO Auto-generated constructor stub
}
#Override
public boolean process(Telemetry telemetry)
{
RequestTelemetry reqTel = (RequestTelemetry) telemetry;
if(reqTel.getResponseCode().equals(HttpStatus.NO_CONTENT.toString()))
return false;
else
return true;
}
}
NOTE: dont forget appropriate type check
I have the gRPC server code as below:
public void buildServer() {
List<BindableService> theServiceList = new ArrayList<BindableService>();
theServiceList.add(new CreateModuleContentService());
theServiceList.add(new RemoveModuleContentService());
ServerBuilder<?> sb = ServerBuilder.forPort(m_port);
for (BindableService aService : theServiceList) {
sb.addService(aService);
}
m_server = sb.build();
}
and client code as below:
public class JavaMainClass {
public static void main(String[] args) {
CreateModuleService createModuleService = new CreateModuleService();
ESDStandardResponse esdReponse = createModuleService.createAtomicBlock("8601934885970354030", "atm1");
RemoveModuleService moduleService = new RemoveModuleService();
moduleService.removeAtomicBlock("8601934885970354030", esdReponse.getId());
}
}
While I am running the client I am getting an exception as below:
Exception in thread "main" io.grpc.StatusRuntimeException: UNIMPLEMENTED: Method grpc.blocks.operations.ModuleContentServices/createAtomicBlock is unimplemented
at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:233)
at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:214)
at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:139)
In the above server class, if I am commenting the line theServiceList.add(new RemoveModuleContentService()); then the CreateModuleContentService service is working fine, also without commenting all the services of RemoveModuleContentService class are working as expected, which means the problem is with the first service when another gets added.
Can someone please suggest how can I add two services to Server Builder.
A particular gRPC service can only be implemented once per server. Since the name of the gRPC service in the error message is ModuleContentServices, I'm assuming CreateModuleContentService and RemoveModuleContentService both extend ModuleContentServicesImplBase.
When you add the same service multiple times, the last one wins. The way the generated code works, every method of a service is registered even if you don't implement that particular method. Every service method defaults to a handler that simply returns "UNIMPLEMENTED: Method X is unimplemented". createAtomicBlock isn't implemented in RemoveModuleContentService, so it returns that error.
If you interact with the ServerServiceDefinition returned by bindService(), you can mix-and-match methods a bit more, but this is a more advanced API and is intended more for frameworks to use because it can become verbose to compose every application service individually.
I'm trying to do URL authorization using a custom AccessDecisionVoter. I don't get any errors and debugging shows that my voter is picked up at start up. However, at runtime, the vote method is not called, thus allowing every authenticated user full access.
Note that, I don't need method security. I'm also not using XML config. That rules out every example ever posted on the internet regarding this topic.
#Configuration
#EnableWebSecurity
#EnableWebMvc
#ComponentScan
#Order(-10)
public class HttpSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${trusted_ports}")
private List<Integer> trustedPorts;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private ServiceIdAwareVoter serviceIdAwareVoter;
RequestMatcher requestMatcher = new OrRequestMatcher(
// #formatter:off
new AntPathRequestMatcher("/**", GET.name()),
new AntPathRequestMatcher("/**", POST.name()),
new AntPathRequestMatcher("/**", DELETE.name()),
new AntPathRequestMatcher("/**", PATCH.name()),
new AntPathRequestMatcher("/**", PUT.name())
// #formatter:on
);
#Override
protected UserDetailsService userDetailsService() {
return userDetailsService;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(preAuthProvider());
auth.authenticationProvider(authProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.
httpBasic().and().
authorizeRequests().anyRequest().fullyAuthenticated().
accessDecisionManager(accessDecisionManager()).and().
csrf().disable().
logout().disable().
exceptionHandling().and().
sessionManagement().sessionCreationPolicy(STATELESS).and().
anonymous().disable().
addFilterAfter(preAuthFilter(), X509AuthenticationFilter.class).
addFilter(authFilter());
// #formatter:on
}
AccessDecisionManager accessDecisionManager() {
return new UnanimousBased(ImmutableList.of(serviceIdAwareVoter));
}
Filter preAuthFilter() throws Exception {
PreAuthenticationFilter preAuthFilter = new PreAuthenticationFilter(trustedPorts);
preAuthFilter.setAuthenticationManager(super.authenticationManager());
return preAuthFilter;
}
PreAuthenticatedAuthenticationProvider preAuthProvider() {
PreAuthenticatedAuthenticationProvider preAuthProvider = new PreAuthenticatedAuthenticationProvider();
UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> userDetailsServiceWrapper = new UserDetailsByNameServiceWrapper<>();
userDetailsServiceWrapper.setUserDetailsService(userDetailsService());
preAuthProvider.setPreAuthenticatedUserDetailsService(userDetailsServiceWrapper);
return preAuthProvider;
}
Filter authFilter() throws Exception {
AppIdAppKeyAuthenticationFilter authFilter = new AppIdAppKeyAuthenticationFilter(requestMatcher);
authFilter.setAuthenticationFailureHandler(new ExceptionStoringAuthenticationFailureHandler());
authFilter.setAuthenticationSuccessHandler(new UrlForwardingAuthenticationSuccessHandler());
authFilter.setAuthenticationManager(authenticationManagerBean());
return authFilter;
}
AuthenticationProvider authProvider() {
AppIdAppKeyAuthenticationProvider authProvider = new AppIdAppKeyAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
return authProvider;
}
Background:
After hours of debugging, I found out the root cause of the problem, which is really deep. Part of it is due to the fact that the Spring Security Java config is very poorly documented (for which I've opened a JIRA ticket). Theirs, as well as most online, examples are copy-pasted from XML config whereas the world has stopped using Spring XML config since probably 2010. Another part is due to the fact that REST service security is an afterthought in the Spring Security design and they don't have first-class support for protecting applications that don't have a login page, error page and the usual view layer. Last but not the least is that there were several (mis)configurations in my app which all came together and created a perfect storm of mind-boggling complexity.
Technical Context:
Using the authorizeRequests() configures a ExpressionUrlAuthorizationConfigurer which ultimately sets up a UnanimousBased AccessDecisionManager with a WebExpressionVoter. This AccessDecisionManager is called from the FilterSecurityInterceptor if the authentication succeeds (obviously there's no point in authorization if the user fails authentication in the first place).
Issues:
In my AbstractAnnotationConfigDispatcherServletInitializer subclass, which is basically the Java version of the web.xml, I'd configured filters not to intercept forward requests. I'm not going to go into the why here. For the interested, here's an example of how it's done:
private Dynamic registerCorsFilter(ServletContext ctx) {
Dynamic registration = ctx.addFilter("CorsFilter", CorsFilter.class);
registration.addMappingForUrlPatterns(getDispatcherTypes(), false, "/*");
return registration;
}
private EnumSet<DispatcherType> getDispatcherTypes() {
return (isAsyncSupported() ? EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ASYNC)
: EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE));
}
If you take the DispatcherType.FORWARD out of the dispatcher types set, the registered filter doesn't kick in for that kind of request.
The authFilter shown in my question extended from UsernamePasswordAuthenticationFilter and had an AuthenticationSuccessHandler which forwarded the request to the destination URL after successful authentication. The default Spring implementation uses a SavedRequestAwareAuthenticationSuccessHandler which does a redirect to a webpage, which is unwanted in the context of a REST app.
Due to the above 2 reasons, the FilterSecurityInterceptor was not invoked after successful authentication which in turn, skipped the authorization chain causing the issue in my original post.
Fix:
Get rid of custom dispatcher configuration from web app initializer.
Don't do forward, or redirect, from AuthenticationSuccessHandler. Just let the request take it's natural course.
The custom voter has a vote method that looks as follows:
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
}
The attributes in my case, as shown in my original post, is the string expression fullyAuthenticated. I didn't use it for authorization as I already knew the user to have been authenticated through the various filters in the authentication flow.
I hope this serves as documentation for all those souls who're suffering from the lack of documentation in Spring Security Java config.
Your config is saying that you are allowing access to fully authenticated users right here:
authorizeRequests().anyRequest().fullyAuthenticated().
You are telling Spring Security to grant access to any request as long as they are fully authenticated. What's you're goal? How are you trying to restrict access, by a role/permission? I'm guessing it's something that you are dictating inside your custom voter bean?
Usually the voter bean comes into play when you have conflicting security levels, for example, here you say that that all requests have full access but if your code hits a method with method level security like this (not a very real-world example):
#PreAuthrorize("permitNone")
public void someMethod{
...
}
You're going to have voters come into play because your java security config is saying "grant access to everyone" (voting yes to access) but this method annotation is "grant access to no one" (voting no to access).
In your case, there's nothing to vote on, you are granting everyone access.
long time ASP.Net interface developer being asked to learn WCF, looking for some education on more architecture related fronts - as its not my strong suit but I'm having to deal.
In our current ASMX world we adopted a model of creating ServiceManager static classes for our interaction with web services. We're starting to migrate to WCF, attempting to follow the same model. At first I was dealing with performance problems, but I've tweaked a bit and we're running smoothly now, but I'm questioning my tactics. Here's a simplified version (removed error handling, caching, object manipulation, etc.) of what we're doing:
public static class ContentManager
{
private static StoryManagerClient _clientProxy = null;
const string _contentServiceResourceCode = "StorySvc";
// FOR CACHING
const int _getStoriesTTL = 300;
private static Dictionary<string, GetStoriesCacheItem> _getStoriesCache = new Dictionary<string, GetStoriesCacheItem>();
private static ReaderWriterLockSlim _cacheLockStories = new ReaderWriterLockSlim();
public static Story[] GetStories(string categoryGuid)
{
// OMITTED - if category is cached and not expired, return from cache
// get endpoint address from FinderClient (ResourceManagement SVC)
UrlResource ur = FinderClient.GetUrlResource(_contentServiceResourceCode);
// Get proxy
StoryManagerClient svc = GetStoryServiceClient(ur.Url);
// create request params
GetStoriesRequest request = new GetStoriesRequest{}; // SIMPLIFIED
Manifest manifest = new Manifest{}; // SIMPLIFIED
// execute GetStories at WCF service
try
{
GetStoriesResponse response = svc.GetStories(manifest, request);
}
catch (Exception)
{
if (svc.State == CommunicationState.Faulted)
{
svc.Abort();
}
throw;
}
// OMITTED - do stuff with response, cache if needed
// return....
}
internal static StoryManagerClient GetStoryServiceClient(string endpointAddress)
{
if (_clientProxy == null)
_clientProxy = new StoryManagerClient(GetServiceBinding(_contentServiceResourceCode), new EndpointAddress(endpointAddress));
return _clientProxy;
}
public static Binding GetServiceBinding(string bindingSettingName)
{
// uses Finder service to load a binding object - our alternative to definition in web.config
}
public static void PreloadContentServiceClient()
{
// get finder location
UrlResource ur = FinderClient.GetUrlResource(_contentServiceResourceCode);
// preload proxy
GetStoryServiceClient(ur.Url);
}
}
We're running smoothly now with round-trip calls completing in the 100ms range. Creating the PreloadContentServiceClient() method and adding to our global.asax got that "first call" performance down to that same level. And you might want to know we're using the DataContractSerializer, and the "Add Service Reference" method.
I've done a lot of reading on static classes, singletons, shared data contract assemblies, how to use the ChannelFactory pattern and a whole bunch of other things that I could do to our usage model...admittedly, some of its gone over my head. And, like I said, we seem to be running smoothly. I know I'm not seeing the big picture, though. Can someone tell me what I've ended up here with regards to channel pooling, proxy failures, etc. and why I should head down the ChannelFactory path? My gut says to just do it, but my head can't comprehend why...
Thanks!
ChannelFactory is typically used when you aren't using Add Service Reference - you have the contract via a shared assembly not generated via a WSDL. Add Service Reference uses ClientBase which is essentially creating the WCF channel for you behind the scenes.
When you are dealing with REST-ful services, WebChannelFactory provides a service-client like interface based off the shared assembly contract. You can't use Add Service Reference if your service only supports a REST-ful endpoint binding.
The only difference to you is preference - do you need full access the channel for custom behaviors, bindings, etc. or does Add Service Reference + SOAP supply you with enough of an interface for your needs.