remove deprecated modules (#10215)
* removed from `compiler`:
* lists (deprecated 2 years ago)
* removed from `lib` (all deprecated 3 years ago):
* ssl
* matchers
* httpserver
* removed from `lib/deprecated`:
* unsigned
* actors (and three accompanying tests)
* parseurl
* moved to `lib/deprecated`:
* securehash (the reason for not directly removing - it was deprecated (only) one year ago)
This commit is contained in:
parent
be9d1280ae
commit
5345c5b130
19 changed files with 5 additions and 1194 deletions
|
|
@ -1,535 +0,0 @@
|
|||
#
|
||||
#
|
||||
# Nim's Runtime Library
|
||||
# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
## This module implements a simple HTTP-Server.
|
||||
##
|
||||
## **Warning**: This module will soon be deprecated in favour of
|
||||
## the ``asyncdispatch`` module, you should use it instead.
|
||||
##
|
||||
## Example:
|
||||
##
|
||||
## .. code-block:: nim
|
||||
## import strutils, sockets, httpserver
|
||||
##
|
||||
## var counter = 0
|
||||
## proc handleRequest(client: Socket, path, query: string): bool {.procvar.} =
|
||||
## inc(counter)
|
||||
## client.send("Hello for the $#th time." % $counter & wwwNL)
|
||||
## return false # do not stop processing
|
||||
##
|
||||
## run(handleRequest, Port(80))
|
||||
##
|
||||
|
||||
import parseutils, strutils, os, osproc, strtabs, streams, sockets, asyncio
|
||||
|
||||
const
|
||||
wwwNL* = "\r\L"
|
||||
ServerSig = "Server: httpserver.nim/1.0.0" & wwwNL
|
||||
|
||||
# --------------- output messages --------------------------------------------
|
||||
|
||||
proc sendTextContentType(client: Socket) =
|
||||
send(client, "Content-type: text/html" & wwwNL)
|
||||
send(client, wwwNL)
|
||||
|
||||
proc sendStatus(client: Socket, status: string) =
|
||||
send(client, "HTTP/1.1 " & status & wwwNL)
|
||||
|
||||
proc badRequest(client: Socket) =
|
||||
# Inform the client that a request it has made has a problem.
|
||||
send(client, "HTTP/1.1 400 Bad Request" & wwwNL)
|
||||
sendTextContentType(client)
|
||||
send(client, "<p>Your browser sent a bad request, " &
|
||||
"such as a POST without a Content-Length.</p>" & wwwNL)
|
||||
|
||||
when false:
|
||||
proc cannotExec(client: Socket) =
|
||||
send(client, "HTTP/1.1 500 Internal Server Error" & wwwNL)
|
||||
sendTextContentType(client)
|
||||
send(client, "<P>Error prohibited CGI execution." & wwwNL)
|
||||
|
||||
proc headers(client: Socket, filename: string) =
|
||||
# XXX could use filename to determine file type
|
||||
send(client, "HTTP/1.1 200 OK" & wwwNL)
|
||||
send(client, ServerSig)
|
||||
sendTextContentType(client)
|
||||
|
||||
proc notFound(client: Socket) =
|
||||
send(client, "HTTP/1.1 404 NOT FOUND" & wwwNL)
|
||||
send(client, ServerSig)
|
||||
sendTextContentType(client)
|
||||
send(client, "<html><title>Not Found</title>" & wwwNL)
|
||||
send(client, "<body><p>The server could not fulfill" & wwwNL)
|
||||
send(client, "your request because the resource specified" & wwwNL)
|
||||
send(client, "is unavailable or nonexistent.</p>" & wwwNL)
|
||||
send(client, "</body></html>" & wwwNL)
|
||||
|
||||
proc unimplemented(client: Socket) =
|
||||
send(client, "HTTP/1.1 501 Method Not Implemented" & wwwNL)
|
||||
send(client, ServerSig)
|
||||
sendTextContentType(client)
|
||||
send(client, "<html><head><title>Method Not Implemented" &
|
||||
"</title></head>" &
|
||||
"<body><p>HTTP request method not supported.</p>" &
|
||||
"</body></HTML>" & wwwNL)
|
||||
|
||||
# ----------------- file serving ---------------------------------------------
|
||||
|
||||
when false:
|
||||
proc discardHeaders(client: Socket) = skip(client)
|
||||
|
||||
proc serveFile*(client: Socket, filename: string) =
|
||||
## serves a file to the client.
|
||||
var f: File
|
||||
if open(f, filename):
|
||||
headers(client, filename)
|
||||
const bufSize = 8000 # != 8K might be good for memory manager
|
||||
var buf = alloc(bufsize)
|
||||
while true:
|
||||
var bytesread = readBuffer(f, buf, bufsize)
|
||||
if bytesread > 0:
|
||||
var byteswritten = send(client, buf, bytesread)
|
||||
if bytesread != bytesWritten:
|
||||
dealloc(buf)
|
||||
close(f)
|
||||
raiseOSError(osLastError())
|
||||
if bytesread != bufSize: break
|
||||
dealloc(buf)
|
||||
close(f)
|
||||
else:
|
||||
notFound(client)
|
||||
|
||||
# ------------------ CGI execution -------------------------------------------
|
||||
when false:
|
||||
# TODO: Fix this, or get rid of it.
|
||||
type
|
||||
RequestMethod = enum reqGet, reqPost
|
||||
|
||||
proc executeCgi(client: Socket, path, query: string, meth: RequestMethod) =
|
||||
var env = newStringTable(modeCaseInsensitive)
|
||||
var contentLength = -1
|
||||
case meth
|
||||
of reqGet:
|
||||
discardHeaders(client)
|
||||
|
||||
env["REQUEST_METHOD"] = "GET"
|
||||
env["QUERY_STRING"] = query
|
||||
of reqPost:
|
||||
var buf = TaintedString""
|
||||
var dataAvail = false
|
||||
while dataAvail:
|
||||
dataAvail = recvLine(client, buf) # TODO: This is incorrect.
|
||||
var L = toLowerAscii(buf.string)
|
||||
if L.startsWith("content-length:"):
|
||||
var i = len("content-length:")
|
||||
while L[i] in Whitespace: inc(i)
|
||||
contentLength = parseInt(substr(L, i))
|
||||
|
||||
if contentLength < 0:
|
||||
badRequest(client)
|
||||
return
|
||||
|
||||
env["REQUEST_METHOD"] = "POST"
|
||||
env["CONTENT_LENGTH"] = $contentLength
|
||||
|
||||
send(client, "HTTP/1.0 200 OK" & wwwNL)
|
||||
|
||||
var process = startProcess(command=path, env=env)
|
||||
if meth == reqPost:
|
||||
# get from client and post to CGI program:
|
||||
var buf = alloc(contentLength)
|
||||
if recv(client, buf, contentLength) != contentLength:
|
||||
dealloc(buf)
|
||||
raiseOSError()
|
||||
var inp = process.inputStream
|
||||
inp.writeData(buf, contentLength)
|
||||
dealloc(buf)
|
||||
|
||||
var outp = process.outputStream
|
||||
var line = newStringOfCap(120).TaintedString
|
||||
while true:
|
||||
if outp.readLine(line):
|
||||
send(client, line.string)
|
||||
send(client, wwwNL)
|
||||
elif not running(process): break
|
||||
|
||||
# --------------- Server Setup -----------------------------------------------
|
||||
|
||||
proc acceptRequest(client: Socket) =
|
||||
var cgi = false
|
||||
var query = ""
|
||||
var buf = TaintedString""
|
||||
discard recvLine(client, buf)
|
||||
var path = ""
|
||||
var data = buf.string.split()
|
||||
var meth = reqGet
|
||||
|
||||
var q = find(data[1], '?')
|
||||
|
||||
# extract path
|
||||
if q >= 0:
|
||||
# strip "?..." from path, this may be found in both POST and GET
|
||||
path = "." & data[1].substr(0, q-1)
|
||||
else:
|
||||
path = "." & data[1]
|
||||
# path starts with "/", by adding "." in front of it we serve files from cwd
|
||||
|
||||
if cmpIgnoreCase(data[0], "GET") == 0:
|
||||
if q >= 0:
|
||||
cgi = true
|
||||
query = data[1].substr(q+1)
|
||||
elif cmpIgnoreCase(data[0], "POST") == 0:
|
||||
cgi = true
|
||||
meth = reqPost
|
||||
else:
|
||||
unimplemented(client)
|
||||
|
||||
if path[path.len-1] == '/' or existsDir(path):
|
||||
path = path / "index.html"
|
||||
|
||||
if not existsFile(path):
|
||||
discardHeaders(client)
|
||||
notFound(client)
|
||||
else:
|
||||
when defined(Windows):
|
||||
var ext = splitFile(path).ext.toLowerAscii
|
||||
if ext == ".exe" or ext == ".cgi":
|
||||
# XXX: extract interpreter information here?
|
||||
cgi = true
|
||||
else:
|
||||
if {fpUserExec, fpGroupExec, fpOthersExec} * path.getFilePermissions != {}:
|
||||
cgi = true
|
||||
if not cgi:
|
||||
serveFile(client, path)
|
||||
else:
|
||||
executeCgi(client, path, query, meth)
|
||||
|
||||
type
|
||||
Server* = object of RootObj ## contains the current server state
|
||||
socket: Socket
|
||||
port: Port
|
||||
client*: Socket ## the socket to write the file data to
|
||||
reqMethod*: string ## Request method. GET or POST.
|
||||
path*, query*: string ## path and query the client requested
|
||||
headers*: StringTableRef ## headers with which the client made the request
|
||||
body*: string ## only set with POST requests
|
||||
ip*: string ## ip address of the requesting client
|
||||
|
||||
PAsyncHTTPServer* = ref AsyncHTTPServer
|
||||
AsyncHTTPServer = object of Server
|
||||
asyncSocket: AsyncSocket
|
||||
|
||||
proc open*(s: var Server, port = Port(80), reuseAddr = false) =
|
||||
## creates a new server at port `port`. If ``port == 0`` a free port is
|
||||
## acquired that can be accessed later by the ``port`` proc.
|
||||
s.socket = socket(AF_INET)
|
||||
if s.socket == invalidSocket: raiseOSError(osLastError())
|
||||
if reuseAddr:
|
||||
s.socket.setSockOpt(OptReuseAddr, true)
|
||||
bindAddr(s.socket, port)
|
||||
listen(s.socket)
|
||||
|
||||
if port == Port(0):
|
||||
s.port = getSockName(s.socket)
|
||||
else:
|
||||
s.port = port
|
||||
s.client = invalidSocket
|
||||
s.reqMethod = ""
|
||||
s.body = ""
|
||||
s.path = ""
|
||||
s.query = ""
|
||||
s.headers = {:}.newStringTable()
|
||||
|
||||
proc port*(s: var Server): Port =
|
||||
## get the port number the server has acquired.
|
||||
result = s.port
|
||||
|
||||
proc next*(s: var Server) =
|
||||
## proceed to the first/next request.
|
||||
var client: Socket
|
||||
new(client)
|
||||
var ip: string
|
||||
acceptAddr(s.socket, client, ip)
|
||||
s.client = client
|
||||
s.ip = ip
|
||||
s.headers = newStringTable(modeCaseInsensitive)
|
||||
#headers(s.client, "")
|
||||
var data = ""
|
||||
s.client.readLine(data)
|
||||
if data == "":
|
||||
# Socket disconnected
|
||||
s.client.close()
|
||||
next(s)
|
||||
return
|
||||
var header = ""
|
||||
while true:
|
||||
s.client.readLine(header)
|
||||
if header == "\c\L": break
|
||||
if header != "":
|
||||
var i = 0
|
||||
var key = ""
|
||||
var value = ""
|
||||
i = header.parseUntil(key, ':')
|
||||
inc(i) # skip :
|
||||
i += header.skipWhiteSpace(i)
|
||||
i += header.parseUntil(value, {'\c', '\L'}, i)
|
||||
s.headers[key] = value
|
||||
else:
|
||||
s.client.close()
|
||||
next(s)
|
||||
return
|
||||
|
||||
var i = skipWhitespace(data)
|
||||
if skipIgnoreCase(data, "GET") > 0:
|
||||
s.reqMethod = "GET"
|
||||
inc(i, 3)
|
||||
elif skipIgnoreCase(data, "POST") > 0:
|
||||
s.reqMethod = "POST"
|
||||
inc(i, 4)
|
||||
else:
|
||||
unimplemented(s.client)
|
||||
s.client.close()
|
||||
next(s)
|
||||
return
|
||||
|
||||
if s.reqMethod == "POST":
|
||||
# Check for Expect header
|
||||
if s.headers.hasKey("Expect"):
|
||||
if s.headers["Expect"].toLowerAscii == "100-continue":
|
||||
s.client.sendStatus("100 Continue")
|
||||
else:
|
||||
s.client.sendStatus("417 Expectation Failed")
|
||||
|
||||
# Read the body
|
||||
# - Check for Content-length header
|
||||
if s.headers.hasKey("Content-Length"):
|
||||
var contentLength = 0
|
||||
if parseInt(s.headers["Content-Length"], contentLength) == 0:
|
||||
badRequest(s.client)
|
||||
s.client.close()
|
||||
next(s)
|
||||
return
|
||||
else:
|
||||
var totalRead = 0
|
||||
var totalBody = ""
|
||||
while totalRead < contentLength:
|
||||
var chunkSize = 8000
|
||||
if (contentLength - totalRead) < 8000:
|
||||
chunkSize = (contentLength - totalRead)
|
||||
var bodyData = newString(chunkSize)
|
||||
var octetsRead = s.client.recv(cstring(bodyData), chunkSize)
|
||||
if octetsRead <= 0:
|
||||
s.client.close()
|
||||
next(s)
|
||||
return
|
||||
totalRead += octetsRead
|
||||
totalBody.add(bodyData)
|
||||
if totalBody.len != contentLength:
|
||||
s.client.close()
|
||||
next(s)
|
||||
return
|
||||
|
||||
s.body = totalBody
|
||||
else:
|
||||
badRequest(s.client)
|
||||
s.client.close()
|
||||
next(s)
|
||||
return
|
||||
|
||||
var L = skipWhitespace(data, i)
|
||||
inc(i, L)
|
||||
# XXX we ignore "HTTP/1.1" etc. for now here
|
||||
var query = 0
|
||||
var last = i
|
||||
while last < data.len and data[last] notin Whitespace:
|
||||
if data[last] == '?' and query == 0: query = last
|
||||
inc(last)
|
||||
if query > 0:
|
||||
s.query = data.substr(query+1, last-1)
|
||||
s.path = data.substr(i, query-1)
|
||||
else:
|
||||
s.query = ""
|
||||
s.path = data.substr(i, last-1)
|
||||
|
||||
proc close*(s: Server) =
|
||||
## closes the server (and the socket the server uses).
|
||||
close(s.socket)
|
||||
|
||||
proc run*(handleRequest: proc (client: Socket,
|
||||
path, query: string): bool {.closure.},
|
||||
port = Port(80)) =
|
||||
## encapsulates the server object and main loop
|
||||
var s: Server
|
||||
open(s, port, reuseAddr = true)
|
||||
#echo("httpserver running on port ", s.port)
|
||||
while true:
|
||||
next(s)
|
||||
if handleRequest(s.client, s.path, s.query): break
|
||||
close(s.client)
|
||||
close(s)
|
||||
|
||||
# -- AsyncIO begin
|
||||
|
||||
proc nextAsync(s: PAsyncHTTPServer) =
|
||||
## proceed to the first/next request.
|
||||
var client: Socket
|
||||
new(client)
|
||||
var ip: string
|
||||
acceptAddr(getSocket(s.asyncSocket), client, ip)
|
||||
s.client = client
|
||||
s.ip = ip
|
||||
s.headers = newStringTable(modeCaseInsensitive)
|
||||
#headers(s.client, "")
|
||||
var data = ""
|
||||
s.client.readLine(data)
|
||||
if data == "":
|
||||
# Socket disconnected
|
||||
s.client.close()
|
||||
return
|
||||
var header = ""
|
||||
while true:
|
||||
s.client.readLine(header) # TODO: Very inefficient here. Prone to DOS.
|
||||
if header == "\c\L": break
|
||||
if header != "":
|
||||
var i = 0
|
||||
var key = ""
|
||||
var value = ""
|
||||
i = header.parseUntil(key, ':')
|
||||
inc(i) # skip :
|
||||
if i < header.len:
|
||||
i += header.skipWhiteSpace(i)
|
||||
i += header.parseUntil(value, {'\c', '\L'}, i)
|
||||
s.headers[key] = value
|
||||
else:
|
||||
s.client.close()
|
||||
return
|
||||
|
||||
var i = skipWhitespace(data)
|
||||
if skipIgnoreCase(data, "GET") > 0:
|
||||
s.reqMethod = "GET"
|
||||
inc(i, 3)
|
||||
elif skipIgnoreCase(data, "POST") > 0:
|
||||
s.reqMethod = "POST"
|
||||
inc(i, 4)
|
||||
else:
|
||||
unimplemented(s.client)
|
||||
s.client.close()
|
||||
return
|
||||
|
||||
if s.reqMethod == "POST":
|
||||
# Check for Expect header
|
||||
if s.headers.hasKey("Expect"):
|
||||
if s.headers["Expect"].toLowerAscii == "100-continue":
|
||||
s.client.sendStatus("100 Continue")
|
||||
else:
|
||||
s.client.sendStatus("417 Expectation Failed")
|
||||
|
||||
# Read the body
|
||||
# - Check for Content-length header
|
||||
if s.headers.hasKey("Content-Length"):
|
||||
var contentLength = 0
|
||||
if parseInt(s.headers["Content-Length"], contentLength) == 0:
|
||||
badRequest(s.client)
|
||||
s.client.close()
|
||||
return
|
||||
else:
|
||||
var totalRead = 0
|
||||
var totalBody = ""
|
||||
while totalRead < contentLength:
|
||||
var chunkSize = 8000
|
||||
if (contentLength - totalRead) < 8000:
|
||||
chunkSize = (contentLength - totalRead)
|
||||
var bodyData = newString(chunkSize)
|
||||
var octetsRead = s.client.recv(cstring(bodyData), chunkSize)
|
||||
if octetsRead <= 0:
|
||||
s.client.close()
|
||||
return
|
||||
totalRead += octetsRead
|
||||
totalBody.add(bodyData)
|
||||
if totalBody.len != contentLength:
|
||||
s.client.close()
|
||||
return
|
||||
|
||||
s.body = totalBody
|
||||
else:
|
||||
badRequest(s.client)
|
||||
s.client.close()
|
||||
return
|
||||
|
||||
var L = skipWhitespace(data, i)
|
||||
inc(i, L)
|
||||
# XXX we ignore "HTTP/1.1" etc. for now here
|
||||
var query = 0
|
||||
var last = i
|
||||
while last < data.len and data[last] notin Whitespace:
|
||||
if data[last] == '?' and query == 0: query = last
|
||||
inc(last)
|
||||
if query > 0:
|
||||
s.query = data.substr(query+1, last-1)
|
||||
s.path = data.substr(i, query-1)
|
||||
else:
|
||||
s.query = ""
|
||||
s.path = data.substr(i, last-1)
|
||||
|
||||
proc asyncHTTPServer*(handleRequest: proc (server: PAsyncHTTPServer, client: Socket,
|
||||
path, query: string): bool {.closure, gcsafe.},
|
||||
port = Port(80), address = "",
|
||||
reuseAddr = false): PAsyncHTTPServer =
|
||||
## Creates an Asynchronous HTTP server at ``port``.
|
||||
var capturedRet: PAsyncHTTPServer
|
||||
new(capturedRet)
|
||||
capturedRet.asyncSocket = asyncSocket()
|
||||
capturedRet.asyncSocket.handleAccept =
|
||||
proc (s: AsyncSocket) =
|
||||
nextAsync(capturedRet)
|
||||
let quit = handleRequest(capturedRet, capturedRet.client, capturedRet.path,
|
||||
capturedRet.query)
|
||||
if quit: capturedRet.asyncSocket.close()
|
||||
if reuseAddr:
|
||||
capturedRet.asyncSocket.setSockOpt(OptReuseAddr, true)
|
||||
|
||||
capturedRet.asyncSocket.bindAddr(port, address)
|
||||
capturedRet.asyncSocket.listen()
|
||||
if port == Port(0):
|
||||
capturedRet.port = getSockName(capturedRet.asyncSocket)
|
||||
else:
|
||||
capturedRet.port = port
|
||||
|
||||
capturedRet.client = invalidSocket
|
||||
capturedRet.reqMethod = ""
|
||||
capturedRet.body = ""
|
||||
capturedRet.path = ""
|
||||
capturedRet.query = ""
|
||||
capturedRet.headers = {:}.newStringTable()
|
||||
result = capturedRet
|
||||
|
||||
proc register*(d: Dispatcher, s: PAsyncHTTPServer) =
|
||||
## Registers a ``PAsyncHTTPServer`` with a ``Dispatcher``.
|
||||
d.register(s.asyncSocket)
|
||||
|
||||
proc close*(h: PAsyncHTTPServer) =
|
||||
## Closes the ``PAsyncHTTPServer``.
|
||||
h.asyncSocket.close()
|
||||
|
||||
when not defined(testing) and isMainModule:
|
||||
var counter = 0
|
||||
|
||||
var s: Server
|
||||
open(s, Port(0))
|
||||
echo("httpserver running on port ", s.port)
|
||||
while true:
|
||||
next(s)
|
||||
|
||||
inc(counter)
|
||||
s.client.send("Hello, Andreas, for the $#th time. $# ? $#" % [
|
||||
$counter, s.path, s.query] & wwwNL)
|
||||
|
||||
close(s.client)
|
||||
close(s)
|
||||
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
#
|
||||
#
|
||||
# Nim's Runtime Library
|
||||
# (c) Copyright 2015 Andreas Rumpf
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
## This module contains various string matchers for email addresses, etc.
|
||||
##
|
||||
## **Warning:** This module is deprecated since version 0.14.0.
|
||||
{.deprecated.}
|
||||
|
||||
{.deadCodeElim: on.} # dce option deprecated
|
||||
|
||||
{.push debugger:off .} # the user does not want to trace a part
|
||||
# of the standard library!
|
||||
|
||||
include "system/inclrtl"
|
||||
|
||||
import parseutils, strutils
|
||||
|
||||
proc validEmailAddress*(s: string): bool {.noSideEffect,
|
||||
rtl, extern: "nsuValidEmailAddress".} =
|
||||
## returns true if `s` seems to be a valid e-mail address.
|
||||
## The checking also uses a domain list.
|
||||
const
|
||||
chars = Letters + Digits + {'!','#','$','%','&',
|
||||
'\'','*','+','/','=','?','^','_','`','{','}','|','~','-','.'}
|
||||
var i = 0
|
||||
if i >= s.len or s[i] notin chars or s[i] == '.': return false
|
||||
while i < s.len and s[i] in chars:
|
||||
if i+1 < s.len and s[i] == '.' and s[i+1] == '.': return false
|
||||
inc(i)
|
||||
if i >= s.len or s[i] != '@': return false
|
||||
var j = len(s)-1
|
||||
if j >= 0 and s[j] notin Letters: return false
|
||||
while j >= i and s[j] in Letters: dec(j)
|
||||
inc(i) # skip '@'
|
||||
while i < s.len and s[i] in {'0'..'9', 'a'..'z', '-', '.'}: inc(i)
|
||||
if i != s.len: return false
|
||||
|
||||
var x = substr(s, j+1)
|
||||
if len(x) == 2 and x[0] in Letters and x[1] in Letters: return true
|
||||
case toLowerAscii(x)
|
||||
of "com", "org", "net", "gov", "mil", "biz", "info", "mobi", "name",
|
||||
"aero", "jobs", "museum": return true
|
||||
else: return false
|
||||
|
||||
proc parseInt*(s: string, value: var int, validRange: HSlice[int, int]) {.
|
||||
noSideEffect, rtl, extern: "nmatchParseInt".} =
|
||||
## parses `s` into an integer in the range `validRange`. If successful,
|
||||
## `value` is modified to contain the result. Otherwise no exception is
|
||||
## raised and `value` is not touched; this way a reasonable default value
|
||||
## won't be overwritten.
|
||||
var x = value
|
||||
try:
|
||||
discard parseutils.parseInt(s, x, 0)
|
||||
except OverflowError:
|
||||
discard
|
||||
if x in validRange: value = x
|
||||
|
||||
when isMainModule:
|
||||
doAssert "wuseldusel@codehome.com".validEmailAddress
|
||||
|
||||
{.pop.}
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
|
||||
## This module is a deprecated alias for the ``sha1`` module.
|
||||
{.deprecated.}
|
||||
|
||||
include "../std/sha1"
|
||||
Loading…
Add table
Add a link
Reference in a new issue