checkUpdatesEnabled?[]:dom.p(box(yellow,'Warning: Checking for updates has not been enabled in mox.conf (CheckUpdates: true).',dom.br(),'Make sure you stay up to date through another mechanism!',dom.br(),'You have a responsibility to keep the internet-connected software you run up to date and secure!',dom.br(),'See ',link('https://updates.xmox.nl/changelog'))),
dom.span('Localpart (optional)',attr.title('Must be set if and only if account does not yet exist. The localpart for the user of this domain. E.g. postmaster.')),
dom.br(),
localpart=dom.input(),
),
' ',
dom.submitbutton('Add domain',attr.title('Domain will be added and the config reloaded. You should add the required DNS records after adding the domain.')),
dom.label('Received ID',attr.title('The ID in the Received header that was added during incoming delivery.')),' ',
recvID=dom.input(attr.required('')),' ',
dom.submitbutton('Lookup cid',attr.title('Logging about an incoming message includes an attribute "cid", a counter identifying the transaction related to delivery of the message. The ID in the received header is an encrypted cid, which this form decrypts, after which you can look it up in the logging.')),' ',
dom.p('Note: changing a log level here only changes it for the current process. When mox restarts, it sets the log levels from the configuration file. Change mox.conf to keep the changes.'),
dom.table(
dom.thead(
dom.tr(
dom.th('Package',attr.title('Log levels can be configured per package. E.g. smtpserver, imapserver, dkim, dmarc, tlsrpt, etc.')),
dom.th('Level',attr.title('If you set the log level to "trace", imap and smtp protocol transcripts will be logged. Sensitive authentication is replaced with "***" unless the level is >= "traceauth". Data is masked with "..." unless the level is "tracedata".')),
dom.clickbutton('Save',attr.title('Set new log level for package.'),asyncfunctionclick(e: MouseEvent){
e.preventDefault()
consttarget=e.target!asHTMLButtonElement
try{
target.disabled=true
awaitclient.LogLevelSet(t[0],lvl.value)
}catch(err){
console.log({err})
window.alert('Error: '+err)
return
}finally{
target.disabled=false
}
window.location.reload()// todo: reload just the current loglevels
}),
' ',
dom.clickbutton('Remove',attr.title('Remove this log level, the default log level will apply.'),t[0]===''?attr.disabled(''):[],asyncfunctionclick(e: MouseEvent){
e.preventDefault()
consttarget=e.target!asHTMLButtonElement
try{
target.disabled=true
awaitclient.LogLevelRemove(t[0])
}catch(err){
console.log({err})
window.alert('Error: '+err)
return
}finally{
target.disabled=false
}
window.location.reload()// todo: reload just the current loglevels
}),
),
)
}),
),
),
dom.br(),
dom.h2('Add log level setting'),
form=dom.form(
asyncfunctionsubmit(e: SubmitEvent){
e.preventDefault()
e.stopPropagation()
fieldset.disabled=true
try{
awaitclient.LogLevelSet(pkg.value,level.value)
}catch(err){
console.log({err})
window.alert('Error: '+errmsg(err))
return
}finally{
fieldset.disabled=false
}
form.reset()
window.location.reload()// todo: reload just the current loglevels
if(!window.confirm('Are you sure you want to remove this address?')){
return
}
consttarget=e.target!asHTMLButtonElement
target.disabled=true
try{
letaddr=k
if(!addr.includes('@')){
addr+='@'+config.Domain
}
awaitclient.AddressRemove(addr)
}catch(err){
console.log({err})
window.alert('Error: '+errmsg(err))
return
}finally{
target.disabled=false
}
window.location.reload()// todo: reload just the list
}),
),
)
})
),
),
dom.br(),
dom.h2('Add address'),
form=dom.form(
asyncfunctionsubmit(e: SubmitEvent){
e.preventDefault()
e.stopPropagation()
fieldset.disabled=true
try{
letaddr=email.value
if(!addr.includes('@')){
if(!config.Domain){
thrownewError('no default domain configured for account')
}
addr+='@'+config.Domain
}
awaitclient.AddressAdd(addr,name)
}catch(err){
console.log({err})
window.alert('Error: '+errmsg(err))
return
}finally{
fieldset.disabled=false
}
form.reset()
window.location.reload()// todo: only reload the destinations
},
fieldset=dom.fieldset(
dom.label(
style({display:'inline-block'}),
dom.span('Email address or localpart',attr.title('If empty, or localpart is empty, a catchall address is configured for the domain.')),
dom.br(),
email=dom.input(),
),
' ',
dom.submitbutton('Add address'),
),
),
dom.br(),
dom.h2('Limits'),
dom.form(
fieldsetLimits=dom.fieldset(
dom.label(
style({display:'block',marginBottom:'.5ex'}),
dom.span('Maximum outgoing messages per day',attr.title('Maximum number of outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 1000. MaxOutgoingMessagesPerDay in configuration file.')),
dom.span('Maximum first-time recipients per day',attr.title('Maximum number of first-time recipients in outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 200. MaxFirstTimeRecipientsPerDay in configuration file.')),
dom.span('Disk usage quota: Maximum total message size ',attr.title('Default maximum total message size in bytes for the account, overriding any globally configured default maximum size if non-zero. A negative value can be used to have no limit in case there is a limit by default. Attempting to add new messages to an account beyond its maximum total size will result in an error. Useful to prevent a single account from filling storage.')),
box(yellow,'Important: Bots will try to bruteforce your password. Connections with failed authentication attempts will be rate limited but attackers WILL find weak passwords. If your account is compromised, spammers are likely to abuse your system, spamming your address and the wider internet in your name. So please pick a random, unguessable password, preferrably at least 12 characters.'),
dom.p('If autoconfig/autodiscover does not work with an email client, use the settings below for this domain. Authenticate with email address and password. ',dom.span('Explicitly configure',attr.title('To prevent authentication mechanism downgrade attempts that may result in clients sending plain text passwords to a MitM.')),' the first supported authentication mechanism: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1, CRAM-MD5.'),
dom.p('DMARC reports are periodically sent by other mail servers that received an email message with a "From" header with our domain. Domains can have a DMARC DNS record that asks other mail servers to send these aggregate reports for analysis.'),
dom.p('Below a summary of DMARC aggregate reporting results for the past 30 days.'),
summaries.length===0?dom.div(box(yellow,'No domains with reports.')):
dom.table(
dom.thead(
dom.tr(
dom.th('Domain',attr.title('Domain to which the DMARC policy applied. If example.com has a DMARC policy, and email is sent with a From-header with subdomain.example.com, and there is no DMARC record for that subdomain, but there is one for example.com, then the DMARC policy of example.com applies and reports are sent for that that domain.')),
dom.th('Messages',attr.title('Total number of messages that had the DMARC policy applied and reported. Actual messages sent is likely higher because not all email servers send DMARC aggregate reports, or perform DMARC checks at all.')),
dom.th('DMARC "quarantine"/"reject"',attr.title('Messages for which policy was to mark them as spam (quarantine) or reject them during SMTP delivery.')),
dom.th('DKIM "fail"',attr.title('Messages with a failing DKIM check. This can happen when sending through a mailing list where that list keeps your address in the message From-header but also strips DKIM-Signature headers in the message. DMARC evaluation passes if either DKIM passes or SPF passes.')),
dom.th('SPF "fail"',attr.title('Message with a failing SPF check. This can happen with email forwarding and with mailing list. Other mail servers have sent email with this domain in the message From-header. DMARC evaluation passes if at least SPF or DKIM passes.')),
dom.th('Policy overrides',attr.title('Mail servers can override the DMARC policy. E.g. a mail server may be able to detect emails coming from mailing lists that do not pass DMARC and would have to be rejected, but for which an override has been configured.')),
dom.p('Incoming messages are checked against the DMARC policy of the domain in the message From header. If the policy requests reporting on the resulting evaluations, they are stored in the database. Each interval of 1 to 24 hours, the evaluations may be sent to a reporting address specified in the domain\'s DMARC policy. Not all evaluations are a reason to send a report, but if a report is sent all evaluations are included.'),
dom.table(dom._class('hover'),
dom.thead(
dom.tr(
dom.th('Domain',attr.title('Domain in the message From header. Keep in mind these can be forged, so this does not necessarily mean someone from this domain authentically tried delivering email.')),
dom.th('Dispositions',attr.title('Unique dispositions occurring in report.')),
dom.th('Evaluations',attr.title('Total number of message delivery attempts, including retries.')),
dom.th('Send report',attr.title('Whether the current evaluations will cause a report to be sent.')),
dom.p('In practice, sending a DMARC report to a reporting address can cause DSN to be sent back. Such addresses can be added to a supression list for a period, to reduce noise in the postmaster mailbox.'),
window.location.reload()// todo: only clear the table?
}catch(err){
console.log({err})
window.alert('Error: '+errmsg(err))
}finally{
target.disabled=false
}
}),
),
dom.br(),
dom.p('The evaluations below will be sent in a DMARC aggregate report to the addresses found in the published DMARC DNS record, which is fetched again before sending the report. The fields Interval hours, Addresses and Policy are only filled for the first row and whenever a new value in the published DMARC record is encountered.'),
dom.table(dom._class('hover'),
dom.thead(
dom.tr(
dom.th('ID'),
dom.th('Evaluated'),
dom.th('Optional',attr.title('Some evaluations will not cause a DMARC aggregate report to be sent. But if a report is sent, optional records are included.')),
dom.th('Interval hours',attr.title('DMARC policies published by a domain can specify how often they would like to receive reports. The default is 24 hours, but can be as often as each hour. To keep reports comparable between different mail servers that send reports, reports are sent at rounded up intervals of whole hours that can divide a 24 hour day, and are aligned with the start of a day at UTC.')),
dom.th('Addresses',attr.title('Addresses that will receive the report. An address can have a maximum report size configured. If there is no address, no report will be sent.')),
dom.th('Policy',attr.title('Summary of the policy as encountered in the DMARC DNS record of the domain, and used for evaluation.')),
dom.th('IP',attr.title('IP address of delivery attempt that was evaluated, relevant for SPF.')),
dom.th('Disposition',attr.title('Our decision to accept/reject this message. It may be different than requested by the published policy. For example, when overriding due to delivery from a mailing list or forwarded address.')),
dom.th('Aligned DKIM/SPF',attr.title('Whether DKIM and SPF had an aligned pass, where strict/relaxed alignment means whether the domain of an SPF pass and DKIM pass matches the exact domain (strict) or optionally a subdomain (relaxed). A DMARC pass requires at least one pass.')),
dom.th('Envelope to',attr.title('Domain used in SMTP RCPT TO during delivery.')),
dom.th('Envelope from',attr.title('Domain used in SMTP MAIL FROM during delivery.')),
dom.th('Message from',attr.title('Domain in "From" message header.')),
dom.th('DKIM details',attr.title('Results of verifying DKIM-Signature headers in message. Only signatures with matching organizational domain are included, regardless of strict/relaxed DKIM alignment in DMARC policy.')),
dom.th('SPF details',attr.title('Results of SPF check used in DMARC evaluation. "mfrom" indicates the "SMTP MAIL FROM" domain was used, "helo" indicates the SMTP EHLO domain was used.')),
// todo future: table sorting? period selection (last day, 7 days, 1 month, 1 year, custom period)? collapse rows for a report? show totals per report? a simple bar graph to visualize messages and dmarc/dkim/spf fails? similar for TLSRPT.
dom.p('DMARC reports are periodically sent by other mail servers that received an email message with a "From" header with our domain. Domains can have a DMARC DNS record that asks other mail servers to send these aggregate reports for analysis.'),
dom.p('Below the DMARC aggregate reports for the past 30 days.'),
(reports||[]).length===0?dom.div('No DMARC reports for domain.'):
dom.table(dom._class('hover'),
dom.thead(
dom.tr(
dom.th('ID'),
dom.th('Organisation',attr.title('Organization that sent the DMARC report.')),
dom.th('Period (UTC)',attr.title('Period this reporting period is about. Mail servers are recommended to stick to whole UTC days.')),
dom.th('Policy',attr.title('The DMARC policy that the remote mail server had fetched and applied to the message. A policy that changed during the reporting period may result in unexpected policy evaluations.')),
dom.th('Source IP',attr.title('Remote IP address of session at remote mail server.')),
dom.th('Messages',attr.title('Total messages that the results apply to.')),
dom.th('ADKIM',attr.title('DKIM alignment. For a pass, one of the DKIM signatures that pass must be strict/relaxed-aligned with the domain, as specified by the policy.')),
dom.th('ASPF',attr.title('SPF alignment. For a pass, the SPF policy must pass and be strict/relaxed-aligned with the domain, as specified by the policy.')),
dom.th('SMTP to',attr.title('Domain of destination address, as specified during the SMTP session.')),
dom.th('SMTP from',attr.title('Domain of originating address, as specified during the SMTP session.')),
dom.th('Header from',attr.title('Domain of address in From-header of message.')),
dom.th('Auth Results',attr.title('Details of DKIM and/or SPF authentication results. DMARC requires at least one aligned DKIM or SPF pass.')),
none:'DMARC checks or were not applied. This does not mean these messages are definitely not spam though, and they may have been rejected based on other checks, such as reputation or content-based filters.',
quarantine:'DMARC policy is to mark message as spam.',
reject:'DMARC policy is to reject the message during SMTP delivery.',
}
constrows: HTMLElement[]=[]
constaddRow=(...last: ElemArg[])=>{
consttr=dom.tr(
recordIndex>0||rows.length>0?[]:[
dom.td(reportRowspan,valignTop,dom.a(''+r.ID,attr.href('#domains/'+d+'/dmarc/'+r.ID),attr.title('View raw report.'))),
(pol.Reasons||[]).map(reason=>[dom.br(),dom.span(reason.Type+(reason.Comment?' ('+reason.Comment+')':''),attr.title('Policy was overridden by remote mail server for this reasons.'))]),
),
dom.td(recordRowspan,valignTop,pol.DKIM==='pass'?'pass':box(yellow,dom.span(pol.DKIM,attr.title('No or no valid DKIM-signature is present that is "aligned" with the domain name.')))),
dom.td(recordRowspan,valignTop,pol.SPF==='pass'?'pass':box(yellow,dom.span(pol.SPF,attr.title('No SPF policy was found, or IP is not allowed by policy, or domain name is not "aligned" with the domain name.')))),
pass:'Message was signed and signature was verified.',
fail:'Message was signed, but signature was invalid.',
policy:'Message was signed, but signature is not accepted by policy.',
neutral:'Message was signed, but the signature contains an error or could not be processed. This status is also used for errors not covered by other statuses.',
temperror:'Message could not be verified. E.g. because of DNS resolve error. A later attempt may succeed. A missing DNS record is treated as temporary error, a new key may not have propagated through DNS shortly after it was taken into use.',
permerror:'Message cannot be verified. E.g. when a required header field is absent or for invalid (combination of) parameters. We typically set this if a DNS record does not allow the signature, e.g. due to algorithm mismatch or expiry.',
dom.span(dkim.Selector,attr.title('Selector, the DKIM record is at "<selector>._domainkey.<domain>".'+(dkim.Domain===d?'':';\ndomain: '+dkim.Domain))),
dom.p('Messages are delivered with SMTP with TLS using STARTTLS if supported and/or required by the recipient domain\'s mail server. TLS connections may fail for various reasons, such as mismatching certificate host name, expired certificates or TLS protocol version/cipher suite incompatibilities. Statistics about successful connections and failed connections are tracked. Results can be tracked for recipient domains (for MTA-STS policies), and per MX host (for DANE). A domain/host can publish a TLSRPT DNS record with addresses that should receive TLS reports. Reports are sent every 24 hours. Not all results are enough reason to send a report, but if a report is sent all results are included. By default, reports are only sent if a report contains a connection failure. Sending reports about all-successful connections can be configured. Reports sent to recipient domains include the results for its MX hosts, and reports for an MX host reference the recipient domains.'),
dom.table(dom._class('hover'),
dom.thead(
dom.tr(
dom.th('Day (UTC)',attr.title('Day covering these results, a whole day from 00:00 UTC to 24:00 UTC.')),
dom.th('Recipient domain',attr.title('Domain of addressee. For delivery to a recipient, the recipient and policy domains will match for reporting on MTA-STS policies, but can also result in reports for hosts from the MX record of the recipient to report on DANE policies.')),
dom.th('Policy domain',attr.title('Domain for TLSRPT policy, specifying URIs to which reports should be sent.')),
dom.th('Host',attr.title('Whether policy domain is an (MX) host (for DANE), or a recipient domain (for MTA-STS).')),
dom.th('Policies',attr.title('Policies found.')),
dom.th('Success',attr.title('Total number of successful connections.')),
dom.th('Failure',attr.title('Total number of failed connection attempts.')),
dom.th('Failure details',attr.title('Total number of details about failures.')),
dom.th('Send report',attr.title('Whether the current results may cause a report to be sent. To prevent report loops, reports are not sent for TLS connections used to deliver TLS or DMARC reports. Whether a report is eventually sent depends on more factors, such as whether the policy domain has a TLSRPT policy with reporting addresses, and whether TLS connection failures were registered (depending on configuration).')),
dom.p('In practice, sending a TLS report to a reporting address can cause DSN to be sent back. Such addresses can be added to a suppress list for a period, to reduce noise in the postmaster mailbox.'),
window.location.reload()// todo: only clear the table?
}catch(err){
console.log({err})
window.alert('Error: '+errmsg(err))
}finally{
target.disabled=false
}
}),
),
dom.br(),
dom.div('Fetching TLSRPT DNS record...'),
recordBox=dom.div(),
dom.br(),
dom.p('Below are the results per day and '+(isrcptdom?'policy':'recipient')+' domain that may be sent in a report.'),
(tlsresults||[]).map(tlsresult=>[
dom.h2(tlsresult.DayUTC,' - ',dom.span(attr.title('Recipient domain, as used in SMTP MAIL TO, usually based on message To/Cc/Bcc.'),isrcptdom?tlsresult.PolicyDomain : tlsresult.RecipientDomain)),
dom.p(
'Send report (if TLSRPT policy exists and has address): '+(tlsresult.SendReport?'Yes':'No'),
dom.br(),
'Report about (MX) host (instead of recipient domain): '+(tlsresult.IsHost?'Yes':'No'),
dom.p('TLSRPT (TLS reporting) is a mechanism to request feedback from other mail servers about TLS connections to your mail server. If is typically used along with MTA-STS and/or DANE to enforce that SMTP connections are protected with TLS. Mail servers implementing TLSRPT will typically send a daily report with both successful and failed connection counts, including details about failures.'),
dom.p('TLSRPT (TLS reporting) is a mechanism to request feedback from other mail servers about TLS connections to your mail server. If is typically used along with MTA-STS and/or DANE to enforce that SMTP connections are protected with TLS. Mail servers implementing TLSRPT will typically send a daily report with both successful and failed connection counts, including details about failures.'),
dom.p('Below the TLS reports for the past 30 days.'),
(records||[]).length===0?dom.div('No TLS reports for domain.'):
dom.table(dom._class('hover'),
dom.thead(
dom.tr(
dom.th('Report',attr.colspan('3')),
dom.th('Policy',attr.colspan('3')),
dom.th('Failure Details',attr.colspan('8')),
),
dom.tr(
dom.th('ID'),
dom.th('From',attr.title('SMTP mail from from which we received the report.')),
dom.th('Period (UTC)',attr.title('Period this reporting period is about. Mail servers are recommended to stick to whole UTC days.')),
dom.th('Policy',attr.title('The policy applied, typically STSv1.')),
dom.th('Successes',attr.title('Total number of successful TLS connections for policy.')),
dom.th('Failures',attr.title('Total number of failed TLS connections for policy.')),
dom.th('Result Type',attr.title('Type of failure.')),
dom.th('Sending MTA',attr.title('IP of sending MTA.')),
dom.th('Receiving MX Host'),
dom.th('Receiving MX HELO'),
dom.th('Receiving IP'),
dom.th('Count',attr.title('Number of TLS connections that failed with these details.')),
dom.th('More',attr.title('Optional additional information about the failure.')),
dom.th('Code',attr.title('Optional API error code relating to the failure.')),
dom.p("MTA-STS is a mechanism allowing email domains to publish a policy for using SMTP STARTTLS and TLS verification. See ",link('https://www.rfc-editor.org/rfc/rfc8461.html','RFC 8461'),'.'),
dom.p("The SMTP protocol is unencrypted by default, though the SMTP STARTTLS command is typically used to enable TLS on a connection. However, MTA's using STARTTLS typically do not validate the TLS certificate. An MTA-STS policy can specify that validation of host name, non-expiration and webpki trust is required."),
// Elements: Field name in JSON, column name override, title for column name.
constkeys=[
["LastUse","","Last time this policy was used."],
["Domain","Domain","Domain this policy was retrieved from and this policy applies to."],
["Backoff","","If true, a DNS record for MTA-STS exists, but a policy could not be fetched. This indicates a failure with MTA-STS."],
["RecordID","","Unique ID for this policy. Each time a domain changes its policy, it must also change the record ID that is published in DNS to propagate the change."],
["Version","","For valid MTA-STS policies, this must be 'STSv1'."],
["Mode","","'enforce': TLS must be used and certificates must be validated; 'none': TLS and certificate validation is not required, typically only useful for removing once-used MTA-STS; 'testing': TLS should be used and certificated should be validated, but fallback to unverified TLS or plain text is allowed, but such cases must be reported"],
["MX","","The MX hosts that are configured to do TLS. If TLS and validation is required, but an MX host is not on this list, delivery will not be attempted to that host."],
["MaxAgeSeconds","","How long a policy can be cached and reused after it was fetched. Typically in the order of weeks."],
["Extensions","","Free-form extensions in the MTA-STS policy."],
["ValidEnd","","Until when this cached policy is valid, based on time the policy was fetched and the policy max age. Non-failure policies are automatically refreshed before they become invalid."],
["LastUpdate","","Last time this policy was updated."],
["Inserted","","Time when the policy was first inserted."],
attr.title('How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'),
// todo: make a mechanism to get the ../config/config.go sconf-doc struct tags
// here. So we can use them for the titles, as documentation. Instead of current
// approach of copy/pasting those texts, inevitably will get out of date.
// todo: perhaps lay these out in the same way as in the config file? will help admins mentally map between the two. will take a bit more vertical screen space, but current approach looks messy/garbled. we could use that mechanism for more parts of the configuration file. we can even show the same sconf-doc struct tags. the html admin page will then just be a glorified guided text editor!
typeHandlerRow={
root: HTMLElement
moveButtons: HTMLElement
get:()=>api.WebHandler
}
// Similar to redirects, but for web handlers.
lethandlerRows: HandlerRow[]=[]
lethandlersTbody: HTMLElement
letnohandler: HTMLElement
typeWebStaticView={
root: HTMLElement
get:()=>api.WebStatic
}
typeWebRedirectView={
root: HTMLElement
get:()=>api.WebRedirect
}
typeWebForwardView={
root: HTMLElement
get:()=>api.WebForward
}
// Make a handler row. This is more complicated, since it can be one of the three
// types (static, redirect, forward), and can change between those types.
consthandlerRow=(wh: api.WebHandler)=>{
letrow: HandlerRow// Shared between the handler types.
attr.title('Path to strip from the request URL before evaluating to a local path. If the requested URL path does not start with this prefix and ContinueNotFound it is considered non-matching and next WebHandlers are tried. If ContinueNotFound is not set, a file not found (404) is returned in that case.'),
),
dom.td(
'Root',
attr.title('Directory to serve files from for this handler. Keep in mind that relative paths are relative to the working directory of mox.'),
),
dom.td(
'ListFiles',
attr.title('If set, and a directory is requested, and no index.html is present that can be served, a file listing is returned. Results in 403 if ListFiles is not set. If a directory is requested and the URL does not end with a slash, the response is a redirect to the path with trailing slash.'),
),
dom.td(
'ContinueNotFound',
attr.title("If a requested URL does not exist, don't return a file not found (404) response, but consider this handler non-matching and continue attempts to serve with later WebHandlers, which may be a reverse proxy generating dynamic content, possibly even writing a static file for a next request to serve statically. If ContinueNotFound is set, HTTP requests other than GET and HEAD do not match. This mechanism can be used to implement the equivalent of 'try_files' in other webservers."),
),
dom.td(
dom.span(
'Response headers',
attr.title('Headers to add to the response. Useful for cache-control, content-type, etc. By default, Content-Type headers are automatically added for recognized file types, unless added explicitly through this setting. For directory listings, a content-type header is skipped.'),
attr.title('Base URL to redirect to. The path must be empty and will be replaced, either by the request URL path, or by OrigPathRegexp/ReplacePath. Scheme, host, port and fragment stay intact, and query strings are combined. If empty, the response redirects to a different path through OrigPathRegexp and ReplacePath, which must then be set. Use a URL without scheme to redirect without changing the protocol, e.g. //newdomain/. If a redirect would send a request to a URL with the same scheme, host and path, the WebRedirect does not match so a next WebHandler can be tried. This can be used to redirect all plain http traffic to https.'),
),
dom.td(
'OrigPathRegexp',
attr.title('Regular expression for matching path. If set and path does not match, a 404 is returned. The HTTP path used for matching always starts with a slash.'),
),
dom.td(
'ReplacePath',
attr.title("Replacement path for destination URL based on OrigPathRegexp. Implemented with Go's Regexp.ReplaceAllString: $1 is replaced with the text of the first submatch, etc. If both OrigPathRegexp and ReplacePath are empty, BaseURL must be set and all paths are redirected unaltered."),
),
dom.td(
'StatusCode',
attr.title('Status code to use in redirect, e.g. 307. By default, a permanent redirect (308) is returned.'),
),
),
dom.tr(
dom.td(
dom.select(
attr.required(''),
dom.option('Static'),
dom.option('Redirect',attr.selected('')),
dom.option('Forward'),
functionchange(e: MouseEvent){
makeType((e.target!asHTMLSelectElement).value)
},
),
),
dom.td(
baseURL=dom.input(attr.placeholder('empty or https://target/path?q=1#frag or //target/...'),attr.value(wr.BaseURL||'')),
attr.title('Strip the matching WebHandler path from the WebHandler before forwarding the request.'),
),
dom.td(
'URL',
attr.title("URL to forward HTTP requests to, e.g. http://127.0.0.1:8123/base. If StripPath is false the full request path is added to the URL. Host headers are sent unmodified. New X-Forwarded-{For,Host,Proto} headers are set. Any query string in the URL is ignored. Requests are made using Go's net/http.DefaultTransport that takes environment variables HTTP_PROXY and HTTPS_PROXY into account. Websocket connections are forwarded and data is copied between client and backend without looking at the framing. The websocket 'version' and 'key'/'accept' headers are verified during the handshake, but other websocket headers, including 'origin', 'protocol' and 'extensions' headers, are not inspected and the backend is responsible for verifying/interpreting them."),
),
dom.td(
dom.span(
'Response headers',
attr.title('Headers to add to the response. Useful for adding security- and cache-related headers.'),
// Row that starts starts with two tables: one for the fields all WebHandlers have
// (in common). And one for the details, i.e. WebStatic, WebRedirect, WebForward.
constroot=dom.tr(
dom.td(
dom.table(
dom.tr(
dom.td('LogName',attr.title('Name used during logging for requests matching this handler. If empty, the index of the handler in the list is used.')),
dom.td('Domain',attr.title('Request must be for this domain to match this handler.')),
dom.td('Path Regexp',attr.title('Request must match this path regular expression to match this handler. Must start with with a ^.')),
dom.td('To HTTPS',attr.title('Redirect plain HTTP (non-TLS) requests to HTTPS.')),
dom.td('Compress',attr.title('Transparently compress responses (currently with gzip) if the client supports it, the status is 200 OK, no Content-Encoding is set on the response yet and the Content-Type of the response hints that the data is compressible (text/..., specific application/... and .../...+json and .../...+xml). For static files only, a cache with compressed files is kept.')),
dom.h2('Handlers',attr.title('Corresponds with WebHandlers in domains.conf')),
dom.p('Each incoming request is check against these handlers, in order. The first matching handler serves the request. Don\'t forget to save after making a change.'),
dom.table(dom._class('long'),
dom.thead(
dom.tr(
dom.th(),
dom.th(handlerActions()),
),
),
handlersTbody=dom.tbody(
(conf.WebHandlers||[]).map(wh=>handlerRow(wh)),
nohandler=dom.tr(
style({display: handlerRows.length?'none':''}),
dom.td(attr.colspan('2'),'No handlers.'),
),
),
dom.tfoot(
dom.tr(
dom.th(),
dom.th(handlerActions()),
),
),
),
dom.br(),
dom.submitbutton('Save',attr.title('Save config. If the configuration has changed since this page was loaded, an error will be returned. After saving, the changes take effect immediately.')),