分类 技术 下的文章

你可能已经熟悉使用 ssh 命令访问远程系统。ssh 命令背后所使用的协议允许终端的输入和输出流经安全通道。但是你知道也可以使用 ssh 来安全地发送和接收其他数据吗?一种方法是使用“ 端口转发 port forwarding ”,它允许你在进行 ssh 会话时安全地连接网络端口。本文向你展示了它是如何工作的。

关于端口

标准 Linux 系统已分配了一组网络端口,范围是 0 - 65535。系统会保留 0 - 1023 的端口以供系统使用。在许多系统中,你不能选择使用这些低端口号。通常有几个端口用于运行特定的服务。你可以在系统的 /etc/services 文件中找到这些定义。

你可以认为网络端口是类似的物理端口或可以连接到电缆的插孔。端口可以连接到系统上的某种服务,类似物理插孔后面的接线。一个例子是 Apache Web 服务器(也称为 httpd)。对于 HTTP 非安全连接,Web 服务器通常要求在主机系统上使用端口 80,对于 HTTPS 安全连接通常要求使用 443。

当你连接到远程系统(例如,使用 Web 浏览器)时,你是将浏览器“连接”到你的主机上的端口。这通常是一个随机的高端口号,例如 54001。你的主机上的端口连接到远程主机上的端口(例如 443)来访问其安全的 Web 服务器。

那么,当你有这么多可用端口时,为什么还要使用端口转发呢?这是 Web 开发人员生活中的几种常见情况。

本地端口转发

想象一下,你正在名为 remote.example.com 的远程系统上进行 Web 开发。通常,你是通过 ssh 进入此系统的,但是它位于防火墙后面,而且该防火墙很少允许其他类型的访问,并且会阻塞大多数其他端口。要尝试你的网络应用,能够使用浏览器访问远程系统会很有帮助。但是,由于使用了讨厌的防火墙,你无法通过在浏览器中输入 URL 的常规方法来访问它。

本地转发使你可以通过 ssh 连接来建立可通过远程系统访问的端口。该端口在系统上显示为本地端口(因而称为“本地转发”)。

假设你的网络应用在 remote.example.com 的 8000 端口上运行。要将那个系统的 8000 端口本地转发到你系统上的 8000 端口,请在开始会话时将 -L 选项与 ssh 结合使用:

$ ssh -L 8000:localhost:8000 remote.example.com

等等,为什么我们使用 localhost 作为转发目标?这是因为从 remote.example.com 的角度来看,你是在要求主机使用其自己的端口 8000。(回想一下,任何主机通常可以通过网络连接 localhost 而连接到自身。)现在那个端口连接到你系统的 8000 端口了。ssh 会话准备就绪后,将其保持打开状态,然后可以在浏览器中键入 http://localhost:8000 来查看你的 Web 应用。现在,系统之间的流量可以通过 ssh 隧道安全地传输!

如果你有敏锐的眼睛,你可能已经注意到了一些东西。如果我们要 remote.example.com 转发到与 localhost 不同的主机名怎么办?如果它可以访问该网络上另一个系统上的端口,那么通常可以同样轻松地转发该端口。例如,假设你想访问也在该远程网络中的 db.example.com 的 MariaDB 或 MySQL 服务。该服务通常在端口 3306 上运行。因此,即使你无法 ssh 到实际的 db.example.com 主机,你也可以使用此命令将其转发:

$ ssh -L 3306:db.example.com:3306 remote.example.com

现在,你可以在 localhost 上运行 MariaDB 命令,而实际上是在使用 db.example.com 主机。

远程端口转发

远程转发让你可以进行相反操作。想象一下,你正在为办公室的朋友设计一个 Web 应用,并想向他们展示你的工作。不过,不幸的是,你在咖啡店里工作,并且由于网络设置,他们无法通过网络连接访问你的笔记本电脑。但是,你同时使用着办公室的 remote.example.com 系统,并且仍然可在这里登录。你的 Web 应用似乎在本地 5000 端口上运行良好。

远程端口转发使你可以通过 ssh 连接从本地系统建立端口的隧道,并使该端口在远程系统上可用。在开始 ssh 会话时,只需使用 -R 选项:

$ ssh -R 6000:localhost:5000 remote.example.com

现在,当在公司防火墙内的朋友打开浏览器时,他们可以进入 http://remote.example.com:6000 查看你的工作。就像在本地端口转发示例中一样,通信通过 ssh 会话安全地进行。

默认情况下,sshd 守护进程运行在设置的主机上,因此只有该主机可以连接它的远程转发端口。假设你的朋友希望能够让其他 example.com 公司主机上的人看到你的工作,而他们不在 remote.example.com 上。你需要让 remote.example.com 主机的所有者将以下选项之一添加到 /etc/ssh/sshd_config 中:

GatewayPorts yes       # 或
GatewayPorts clientspecified

第一个选项意味着 remote.example.com 上的所有网络接口都可以使用远程转发的端口。第二个意味着建立隧道的客户端可以选择地址。默认情况下,此选项设置为 no

使用此选项,你作为 ssh 客户端仍必须指定可以共享你这边转发端口的接口。通过在本地端口之前添加网络地址范围来进行此操作。有几种方法可以做到,包括:

$ ssh -R *:6000:localhost:5000                   # 所有网络
$ ssh -R 0.0.0.0:6000:localhost:5000             # 所有网络
$ ssh -R 192.168.1.15:6000:localhost:5000        # 单个网络
$ ssh -R remote.example.com:6000:localhost:5000  # 单个网络

其他注意事项

请注意,本地和远程系统上的端口号不必相同。实际上,有时你甚至可能无法使用相同的端口。例如,普通用户可能不会在默认设置中转发到系统端口。

另外,可以限制主机上的转发。如果你需要在联网主机上更严格的安全性,那么这你来说可能很重要。 sshd 守护程进程的 PermitOpen 选项控制是否以及哪些端口可用于 TCP 转发。默认设置为 any,这让上面的所有示例都能正常工作。要禁止任何端口转发,请选择 none,或仅允许的特定的“主机:端口”。有关更多信息,请在手册页中搜索 PermitOpen 来配置 sshd 守护进程:

$ man sshd_config

最后,请记住,只有在 ssh 会话处于打开状态时才会端口转发。如果需要长时间保持转发活动,请尝试使用 -N 选项在后台运行会话。确保控制台已锁定,以防止在你离开控制台时其被篡夺。


via: https://fedoramagazine.org/using-ssh-port-forwarding-on-fedora/

作者:Paul W. Frields 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

开源世界中的每个主要发行版都在演进,逐渐将 nftables 作为了默认防火墙。换言之,古老的 iptables 现在已经消亡。本文是有关如何构建 nftables 的教程。

当前,有一个与 nftables 兼容的 iptables-nft 后端,但是很快,即使是它也不再提供了。另外,正如 Red Hat 开发人员所指出的那样,有时它可能会错误地转换规则。我们需要知道如何构建自己的 nftables,而不是依赖于 iptables 到 nftables 的转换器。

在 nftables 中,所有地址族都遵循一个规则。与 iptables 不同,nftables 在用户空间中运行,iptables 中的每个模块都运行在内核(空间)中。它很少需要更新内核,并带有一些新功能,例如映射、地址族和字典。

地址族

地址族确定要处理的数据包的类型。在 nftables 中有六个地址族,它们是:

  • ip
  • ipv6
  • inet
  • arp
  • bridge
  • netdev

在 nftables 中,ipv4 和 ipv6 协议可以被合并为一个称为 inet 的单一地址族。因此,我们不需要指定两个规则:一个用于 ipv4,另一个用于 ipv6。如果未指定地址族,它将默认为 ip 协议,即 ipv4。我们感兴趣的领域是 inet 地址族,因为大多数家庭用户将使用 ipv4 或 ipv6 协议。

nftables

典型的 nftables 规则包含三个部分:表、链和规则。

表是链和规则的容器。它们由其地址族和名称来标识。链包含 inet/arp/bridge/netdev 等协议所需的规则,并具有三种类型:过滤器、NAT 和路由。nftables 规则可以从脚本加载,也可以在终端键入,然后另存为规则集。

对于家庭用户,默认链为过滤器。inet 系列包含以下钩子:

  • Input
  • Output
  • Forward
  • Pre-routing
  • Post-routing

使用脚本还是不用?

最大的问题之一是我们是否可以使用防火墙脚本。答案是:这是你自己的选择。这里有一些建议:如果防火墙中有数百条规则,那么最好使用脚本,但是如果你是典型的家庭用户,则可以在终端中键入命令,然后(保存并在重启时)加载规则集。每种选择都有其自身的优缺点。在本文中,我们将在终端中键入它们以构建防火墙。

nftables 使用一个名为 nft 的程序来添加、创建、列出、删除和加载规则。确保使用以下命令将 nftables 与 conntrackd 和 netfilter-persistent 软件包一起安装,并删除 iptables:

apt-get install nftables conntrackd netfilter-persistent
apt-get purge iptables

nft 需要以 root 身份运行或使用 sudo 运行。使用以下命令分别列出、刷新、删除规则集和加载脚本。

nft list ruleset
nft flush ruleset
nft delete table inet filter
/usr/sbin/nft -f /etc/nftables.conf

输入策略

就像 iptables 一样,防火墙将包含三部分:输入(input)、转发(forward)和输出(output)。在终端中,为输入(input)策略键入以下命令。在开始之前,请确保已刷新规则集。我们的默认策略将会删除所有内容。我们将在防火墙中使用 inet 地址族。将以下规则以 root 身份添加或使用 sudo 运行:

nft add table inet filter
nft add chain inet filter input { type filter hook input priority 0 \; counter \; policy drop \; }

你会注意到有一个名为 priority 0 的东西。这意味着赋予该规则更高的优先级。挂钩通常赋予负整数,这意味着更高的优先级。每个挂钩都有自己的优先级,过滤器链的优先级为 0。你可以检查 nftables Wiki 页面以查看每个挂钩的优先级。

要了解你计算机中的网络接口,请运行以下命令:

ip link show

它将显示已安装的网络接口,一个是本地主机、另一个是以太网端口或无线端口。以太网端口的名称如下所示:enpXsY,其中 XY 是数字,无线端口也是如此。我们必须允许本地主机的流量,并且仅允许从互联网建立的传入连接。

nftables 具有一项称为裁决语句的功能,用于解析规则。裁决语句为 acceptdropqueuejumpgotocontinuereturn。由于这是一个很简单的防火墙,因此我们将使用 acceptdrop 处理数据包。

nft add rule inet filter input iifname lo accept
nft add rule inet filter input iifname enpXsY ct state new, established, related accept

接下来,我们必须添加规则以保护我们免受隐秘扫描。并非所有的隐秘扫描都是恶意的,但大多数都是。我们必须保护网络免受此类扫描。第一组规则列出了要测试的 TCP 标志。在这些标志中,第二组列出了要与第一组匹配的标志。

nft add rule inet filter input iifname enpXsY tcp flags \& \(syn\|fin\) == \(syn\|fin\) drop
nft add rule inet filter input iifname enpXsY tcp flags \& \(syn\|rst\) == \(syn\|rst\) drop
nft add rule inet filter input iifname enpXsY tcp flags \& \(fin\|rst\) == \(fin\|rst\) drop
nft add rule inet filter input iifname enpXsY tcp flags \& \(ack\|fin\) == fin drop
nft add rule inet filter input iifname enpXsY tcp flags \& \(ack\|psh\) == psh drop
nft add rule inet filter input iifname enpXsY tcp flags \& \(ack\|urg\) == urg drop

记住,我们在终端中键入这些命令。因此,我们必须在一些特殊字符之前添加一个反斜杠,以确保终端能够正确解释该斜杠。如果你使用的是脚本,则不需要这样做。

关于 ICMP 的警告

互联网控制消息协议(ICMP)是一种诊断工具,因此不应完全丢弃该流量。完全阻止 ICMP 的任何尝试都是不明智的,因为它还会导致停止向我们提供错误消息。仅启用最重要的控制消息,例如回声请求、回声应答、目的地不可达和超时等消息,并拒绝其余消息。回声请求和回声应答是 ping 的一部分。在输入策略中,我们仅允许回声应答、而在输出策略中,我们仅允许回声请求。

nft add rule inet filter input iifname enpXsY icmp type { echo-reply, destination-unreachable, time-exceeded } limit rate 1/second accept
nft add rule inet filter input iifname enpXsY ip protocol icmp drop

最后,我们记录并丢弃所有无效数据包。

nft add rule inet filter input iifname enpXsY ct state invalid log flags all level info prefix \”Invalid-Input: \”
nft add rule inet filter input iifname enpXsY ct state invalid drop

转发和输出策略

在转发和输出策略中,默认情况下我们将丢弃数据包,仅接受已建立连接的数据包。

nft add chain inet filter forward { type filter hook forward priority 0 \; counter \; policy drop \; }
nft add rule inet filter forward ct state established, related accept
nft add rule inet filter forward ct state invalid drop
nft add chain inet filter output { type filter hook output priority 0 \; counter \; policy drop \; }

典型的桌面用户只需要端口 80 和 443 即可访问互联网。最后,允许可接受的 ICMP 协议并在记录无效数据包时丢弃它们。

nft add rule inet filter output oifname enpXsY tcp dport { 80, 443 } ct state established accept
nft add rule inet filter output oifname enpXsY icmp type { echo-request, destination-unreachable, time-exceeded } limit rate 1/second accept
nft add rule inet filter output oifname enpXsY ip protocol icmp drop
nft add rule inet filter output oifname enpXsY ct state invalid log flags all level info prefix \”Invalid-Output: \”
nft add rule inet filter output oifname enpXsY ct state invalid drop

现在我们必须保存我们的规则集,否则重新启动时它将丢失。为此,请运行以下命令:

sudo nft list ruleset. > /etc/nftables.conf

我们须在引导时加载 nftables,以下将在 systemd 中启用 nftables 服务:

sudo systemctl enable nftables

接下来,编辑 nftables 单元文件以删除 Execstop 选项,以避免在每次引导时刷新规则集。该文件通常位于 /etc/systemd/system/sysinit.target.wants/nftables.service。现在重新启动nftables:

sudo systemctl restart nftables

在 rsyslog 中记录日志

当你记录丢弃的数据包时,它们直接进入 syslog,这使得读取该日志文件非常困难。最好将防火墙日志重定向到单独的文件。在 /var/log 目录中创建一个名为 nftables 的目录,并在其中创建两个名为 input.logoutput.log 的文件,分别存储输入和输出日志。确保系统中已安装 rsyslog。现在转到 /etc/rsyslog.d 并创建一个名为 nftables.conf 的文件,其内容如下:

:msg,regex,”Invalid-Input: “ -/var/log/nftables/Input.log
:msg,regex,”Invalid-Output: “ -/var/log/nftables/Output.log & stop

现在,我们必须确保日志是可管理的。为此,使用以下代码在 /etc/logrotate.d 中创建另一个名为 nftables 的文件:

/var/log/nftables/* { rotate 5 daily maxsize 50M missingok notifempty delaycompress compress postrotate invoke-rc.d rsyslog rotate > /dev/null endscript }

重新启动 nftables。现在,你可以检查你的规则集。如果你觉得在终端中键入每个命令很麻烦,则可以使用脚本来加载 nftables 防火墙。我希望本文对保护你的系统有用。


via: https://opensourceforu.com/2019/10/transition-to-nftables/

作者:Vijay Marcel D 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

上一篇:模式

在这篇帖子中,我们将会通过为应用添加社交登录功能进入后端开发。

社交登录的工作方式十分简单:用户点击链接,然后重定向到 GitHub 授权页面。当用户授予我们对他的个人信息的访问权限之后,就会重定向回登录页面。下一次尝试登录时,系统将不会再次请求授权,也就是说,我们的应用已经记住了这个用户。这使得整个登录流程看起来就和你用鼠标单击一样快。

如果进一步考虑其内部实现的话,过程就会变得复杂起来。首先,我们需要注册一个新的 GitHub OAuth 应用

这一步中,比较重要的是回调 URL。我们将它设置为 http://localhost:3000/api/oauth/github/callback。这是因为,在开发过程中,我们总是在本地主机上工作。一旦你要将应用交付生产,请使用正确的回调 URL 注册一个新的应用。

注册以后,你将会收到“客户端 id”和“安全密钥”。安全起见,请不要与任何人分享他们 ?

顺便让我们开始写一些代码吧。现在,创建一个 main.go 文件:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "net/url"
    "os"
    "strconv"

    "github.com/gorilla/securecookie"
    "github.com/joho/godotenv"
    "github.com/knq/jwt"
    _ "github.com/lib/pq"
    "github.com/matryer/way"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/github"
)

var origin *url.URL
var db *sql.DB
var githubOAuthConfig *oauth2.Config
var cookieSigner *securecookie.SecureCookie
var jwtSigner jwt.Signer

func main() {
    godotenv.Load()

    port := intEnv("PORT", 3000)
    originString := env("ORIGIN", fmt.Sprintf("http://localhost:%d/", port))
    databaseURL := env("DATABASE_URL", "postgresql://[email protected]:26257/messenger?sslmode=disable")
    githubClientID := os.Getenv("GITHUB_CLIENT_ID")
    githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
    hashKey := env("HASH_KEY", "secret")
    jwtKey := env("JWT_KEY", "secret")

    var err error
    if origin, err = url.Parse(originString); err != nil || !origin.IsAbs() {
        log.Fatal("invalid origin")
        return
    }

    if i, err := strconv.Atoi(origin.Port()); err == nil {
        port = i
    }

    if githubClientID == "" || githubClientSecret == "" {
        log.Fatalf("remember to set both $GITHUB_CLIENT_ID and $GITHUB_CLIENT_SECRET")
        return
    }

    if db, err = sql.Open("postgres", databaseURL); err != nil {
        log.Fatalf("could not open database connection: %v\n", err)
        return
    }
    defer db.Close()
    if err = db.Ping(); err != nil {
        log.Fatalf("could not ping to db: %v\n", err)
        return
    }

    githubRedirectURL := *origin
    githubRedirectURL.Path = "/api/oauth/github/callback"
    githubOAuthConfig = &oauth2.Config{
        ClientID:     githubClientID,
        ClientSecret: githubClientSecret,
        Endpoint:     github.Endpoint,
        RedirectURL:  githubRedirectURL.String(),
        Scopes:       []string{"read:user"},
    }

    cookieSigner = securecookie.New([]byte(hashKey), nil).MaxAge(0)

    jwtSigner, err = jwt.HS256.New([]byte(jwtKey))
    if err != nil {
        log.Fatalf("could not create JWT signer: %v\n", err)
        return
    }

    router := way.NewRouter()
    router.HandleFunc("GET", "/api/oauth/github", githubOAuthStart)
    router.HandleFunc("GET", "/api/oauth/github/callback", githubOAuthCallback)
    router.HandleFunc("GET", "/api/auth_user", guard(getAuthUser))

    log.Printf("accepting connections on port %d\n", port)
    log.Printf("starting server at %s\n", origin.String())
    addr := fmt.Sprintf(":%d", port)
    if err = http.ListenAndServe(addr, router); err != nil {
        log.Fatalf("could not start server: %v\n", err)
    }
}

func env(key, fallbackValue string) string {
    v, ok := os.LookupEnv(key)
    if !ok {
        return fallbackValue
    }
    return v
}

func intEnv(key string, fallbackValue int) int {
    v, ok := os.LookupEnv(key)
    if !ok {
        return fallbackValue
    }
    i, err := strconv.Atoi(v)
    if err != nil {
        return fallbackValue
    }
    return i
}

安装依赖项:

go get -u github.com/gorilla/securecookie
go get -u github.com/joho/godotenv
go get -u github.com/knq/jwt
go get -u github.com/lib/pq
ge get -u github.com/matoous/go-nanoid
go get -u github.com/matryer/way
go get -u golang.org/x/oauth2

我们将会使用 .env 文件来保存密钥和其他配置。请创建这个文件,并保证里面至少包含以下内容:

GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret

我们还要用到的其他环境变量有:

  • PORT:服务器运行的端口,默认值是 3000
  • ORIGIN:你的域名,默认值是 http://localhost:3000/。我们也可以在这里指定端口。
  • DATABASE_URL:Cockroach 数据库的地址。默认值是 postgresql://[email protected]:26257/messenger?sslmode=disable
  • HASH_KEY:用于为 cookie 签名的密钥。没错,我们会使用已签名的 cookie 来确保安全。
  • JWT_KEY:用于签署 JSON 网络令牌 Web Token 的密钥。

因为代码中已经设定了默认值,所以你也不用把它们写到 .env 文件中。

在读取配置并连接到数据库之后,我们会创建一个 OAuth 配置。我们会使用 ORIGIN 信息来构建回调 URL(就和我们在 GitHub 页面上注册的一样)。我们的数据范围设置为 “read:user”。这会允许我们读取公开的用户信息,这里我们只需要他的用户名和头像就够了。然后我们会初始化 cookie 和 JWT 签名器。定义一些端点并启动服务器。

在实现 HTTP 处理程序之前,让我们编写一些函数来发送 HTTP 响应。

func respond(w http.ResponseWriter, v interface{}, statusCode int) {
    b, err := json.Marshal(v)
    if err != nil {
        respondError(w, fmt.Errorf("could not marshal response: %v", err))
        return
    }
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(statusCode)
    w.Write(b)
}

func respondError(w http.ResponseWriter, err error) {
    log.Println(err)
    http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

第一个函数用来发送 JSON,而第二个将错误记录到控制台并返回一个 500 Internal Server Error 错误信息。

OAuth 开始

所以,用户点击写着 “Access with GitHub” 的链接。该链接指向 /api/oauth/github,这将会把用户重定向到 github。

func githubOAuthStart(w http.ResponseWriter, r *http.Request) {
    state, err := gonanoid.Nanoid()
    if err != nil {
        respondError(w, fmt.Errorf("could not generte state: %v", err))
        return
    }

    stateCookieValue, err := cookieSigner.Encode("state", state)
    if err != nil {
        respondError(w, fmt.Errorf("could not encode state cookie: %v", err))
        return
    }

    http.SetCookie(w, &http.Cookie{
        Name:     "state",
        Value:    stateCookieValue,
        Path:     "/api/oauth/github",
        HttpOnly: true,
    })
    http.Redirect(w, r, githubOAuthConfig.AuthCodeURL(state), http.StatusTemporaryRedirect)
}

OAuth2 使用一种机制来防止 CSRF 攻击,因此它需要一个“状态”(state)。我们使用 Nanoid() 来创建一个随机字符串,并用这个字符串作为状态。我们也把它保存为一个 cookie。

OAuth 回调

一旦用户授权我们访问他的个人信息,他将会被重定向到这个端点。这个 URL 的查询字符串上将会包含状态(state)和授权码(code): /api/oauth/github/callback?state=&code=

const jwtLifetime = time.Hour * 24 * 14

type GithubUser struct {
    ID        int     `json:"id"`
    Login     string  `json:"login"`
    AvatarURL *string `json:"avatar_url,omitempty"`
}

type User struct {
    ID        string  `json:"id"`
    Username  string  `json:"username"`
    AvatarURL *string `json:"avatarUrl"`
}

func githubOAuthCallback(w http.ResponseWriter, r *http.Request) {
    stateCookie, err := r.Cookie("state")
    if err != nil {
        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
        return
    }

    http.SetCookie(w, &http.Cookie{
        Name:     "state",
        Value:    "",
        MaxAge:   -1,
        HttpOnly: true,
    })

    var state string
    if err = cookieSigner.Decode("state", stateCookie.Value, &state); err != nil {
        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
        return
    }

    q := r.URL.Query()

    if state != q.Get("state") {
        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
        return
    }

    ctx := r.Context()

    t, err := githubOAuthConfig.Exchange(ctx, q.Get("code"))
    if err != nil {
        respondError(w, fmt.Errorf("could not fetch github token: %v", err))
        return
    }

    client := githubOAuthConfig.Client(ctx, t)
    resp, err := client.Get("https://api.github.com/user")
    if err != nil {
        respondError(w, fmt.Errorf("could not fetch github user: %v", err))
        return
    }

    var githubUser GithubUser
    if err = json.NewDecoder(resp.Body).Decode(&githubUser); err != nil {
        respondError(w, fmt.Errorf("could not decode github user: %v", err))
        return
    }
    defer resp.Body.Close()

    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        respondError(w, fmt.Errorf("could not begin tx: %v", err))
        return
    }

    var user User
    if err = tx.QueryRowContext(ctx, `
        SELECT id, username, avatar_url FROM users WHERE github_id = $1
    `, githubUser.ID).Scan(&user.ID, &user.Username, &user.AvatarURL); err == sql.ErrNoRows {
        if err = tx.QueryRowContext(ctx, `
            INSERT INTO users (username, avatar_url, github_id) VALUES ($1, $2, $3)
            RETURNING id
        `, githubUser.Login, githubUser.AvatarURL, githubUser.ID).Scan(&user.ID); err != nil {
            respondError(w, fmt.Errorf("could not insert user: %v", err))
            return
        }
        user.Username = githubUser.Login
        user.AvatarURL = githubUser.AvatarURL
    } else if err != nil {
        respondError(w, fmt.Errorf("could not query user by github ID: %v", err))
        return
    }

    if err = tx.Commit(); err != nil {
        respondError(w, fmt.Errorf("could not commit to finish github oauth: %v", err))
        return
    }

    exp := time.Now().Add(jwtLifetime)
    token, err := jwtSigner.Encode(jwt.Claims{
        Subject:    user.ID,
        Expiration: json.Number(strconv.FormatInt(exp.Unix(), 10)),
    })
    if err != nil {
        respondError(w, fmt.Errorf("could not create token: %v", err))
        return
    }

    expiresAt, _ := exp.MarshalText()

    data := make(url.Values)
    data.Set("token", string(token))
    data.Set("expires_at", string(expiresAt))

    http.Redirect(w, r, "/callback?"+data.Encode(), http.StatusTemporaryRedirect)
}

首先,我们会尝试使用之前保存的状态对 cookie 进行解码。并将其与查询字符串中的状态进行比较。如果它们不匹配,我们会返回一个 418 I'm teapot(未知来源)错误。

接着,我们使用授权码生成一个令牌。这个令牌被用于创建 HTTP 客户端来向 GitHub API 发出请求。所以最终我们会向 https://api.github.com/user 发送一个 GET 请求。这个端点将会以 JSON 格式向我们提供当前经过身份验证的用户信息。我们将会解码这些内容,一并获取用户的 ID、登录名(用户名)和头像 URL。

然后我们将会尝试在数据库上找到具有该 GitHub ID 的用户。如果没有找到,就使用该数据创建一个新的。

之后,对于新创建的用户,我们会发出一个将用户 ID 作为主题(Subject)的 JSON 网络令牌,并使用该令牌重定向到前端,查询字符串中一并包含该令牌的到期日(Expiration)。

这一 Web 应用也会被用在其他帖子,但是重定向的链接会是 /callback?token=&expires_at=。在那里,我们将会利用 JavaScript 从 URL 中获取令牌和到期日,并通过 Authorization 标头中的令牌以 Bearer token_here 的形式对 /api/auth_user 进行 GET 请求,来获取已认证的身份用户并将其保存到 localStorage。

Guard 中间件

为了获取当前已经过身份验证的用户,我们设计了 Guard 中间件。这是因为在接下来的文章中,我们会有很多需要进行身份认证的端点,而中间件将会允许我们共享这一功能。

type ContextKey struct {
    Name string
}

var keyAuthUserID = ContextKey{"auth_user_id"}

func guard(handler http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var token string
        if a := r.Header.Get("Authorization"); strings.HasPrefix(a, "Bearer ") {
            token = a[7:]
        } else if t := r.URL.Query().Get("token"); t != "" {
            token = t
        } else {
            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

        var claims jwt.Claims
        if err := jwtSigner.Decode([]byte(token), &claims); err != nil {
            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

        ctx := r.Context()
        ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)

        handler(w, r.WithContext(ctx))
    }
}

首先,我们尝试从 Authorization 标头或者是 URL 查询字符串中的 token 字段中读取令牌。如果没有找到,我们需要返回 401 Unauthorized(未授权)错误。然后我们将会对令牌中的申明进行解码,并使用该主题作为当前已经过身份验证的用户 ID。

现在,我们可以用这一中间件来封装任何需要授权的 http.handlerFunc,并且在处理函数的上下文中保有已经过身份验证的用户 ID。

var guarded = guard(func(w http.ResponseWriter, r *http.Request) {
    authUserID := r.Context().Value(keyAuthUserID).(string)
})

获取认证用户

func getAuthUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    authUserID := ctx.Value(keyAuthUserID).(string)

    var user User
    if err := db.QueryRowContext(ctx, `
        SELECT username, avatar_url FROM users WHERE id = $1
    `, authUserID).Scan(&user.Username, &user.AvatarURL); err == sql.ErrNoRows {
        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
        return
    } else if err != nil {
        respondError(w, fmt.Errorf("could not query auth user: %v", err))
        return
    }

    user.ID = authUserID

    respond(w, user, http.StatusOK)
}

我们使用 Guard 中间件来获取当前经过身份认证的用户 ID 并查询数据库。

这一部分涵盖了后端的 OAuth 流程。在下一篇帖子中,我们将会看到如何开始与其他用户的对话。


via: https://nicolasparada.netlify.com/posts/go-messenger-oauth/

作者:Nicolás Parada 选题:lujun9972 译者:PsiACE 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Rsyslog 是一个自由开源的日志记录程序,在 CentOS 8 和 RHEL 8 系统上默认可用。它提供了一种从客户端节点到单个中央服务器的“集中日志”的简单有效的方法。日志集中化有两个好处。首先,它简化了日志查看,因为系统管理员可以在一个中心节点查看远程服务器的所有日志,而无需登录每个客户端系统来检查日志。如果需要监视多台服务器,这将非常有用,其次,如果远程客户端崩溃,你不用担心丢失日志,因为所有日志都将保存在中心的 Rsyslog 服务器上。rsyslog 取代了仅支持 UDP 协议的 syslog。它以优异的功能扩展了基本的 syslog 协议,例如在传输日志时支持 UDP 和 TCP 协议,增强的过滤功能以及灵活的配置选项。让我们来探讨如何在 CentOS 8 / RHEL 8 系统中配置 Rsyslog 服务器。

configure-rsyslog-centos8-rhel8

预先条件

我们将搭建以下实验环境来测试集中式日志记录过程:

  • Rsyslog 服务器 CentOS 8 Minimal IP 地址: 10.128.0.47
  • 客户端系统 RHEL 8 Minimal IP 地址: 10.128.0.48

通过上面的设置,我们将演示如何设置 Rsyslog 服务器,然后配置客户端系统以将日志发送到 Rsyslog 服务器进行监视。

让我们开始!

在 CentOS 8 上配置 Rsyslog 服务器

默认情况下,Rsyslog 已安装在 CentOS 8 / RHEL 8 服务器上。要验证 Rsyslog 的状态,请通过 SSH 登录并运行以下命令:

$ systemctl status rsyslog

示例输出:

rsyslog-service-status-centos8

如果由于某种原因 Rsyslog 不存在,那么可以使用以下命令进行安装:

$ sudo yum install rsyslog

接下来,你需要修改 Rsyslog 配置文件中的一些设置。打开配置文件:

$ sudo vim /etc/rsyslog.conf

滚动并取消注释下面的行,以允许通过 UDP 协议接收日志:

module(load="imudp") # needs to be done just once
input(type="imudp" port="514")

rsyslog-conf-centos8-rhel8

同样,如果你希望启用 TCP rsyslog 接收,请取消注释下面的行:

module(load="imtcp") # needs to be done just once
input(type="imtcp" port="514")

rsyslog-conf-tcp-centos8-rhel8

保存并退出配置文件。

要从客户端系统接收日志,我们需要在防火墙上打开 Rsyslog 默认端口 514。为此,请运行:

# sudo firewall-cmd  --add-port=514/tcp  --zone=public  --permanent

接下来,重新加载防火墙保存更改:

# sudo firewall-cmd --reload

示例输出:

firewall-ports-rsyslog-centos8

接下来,重启 Rsyslog 服务器:

$ sudo systemctl restart rsyslog

要在启动时运行 Rsyslog,运行以下命令:

$ sudo systemctl enable rsyslog

要确认 Rsyslog 服务器正在监听 514 端口,请使用 netstat 命令,如下所示:

$ sudo netstat -pnltu

示例输出:

netstat-rsyslog-port-centos8

完美!我们已经成功配置了 Rsyslog 服务器来从客户端系统接收日志。

要实时查看日志消息,请运行以下命令:

$ tail -f /var/log/messages

现在开始配置客户端系统。

在 RHEL 8 上配置客户端系统

与 Rsyslog 服务器一样,登录并通过以下命令检查 rsyslog 守护进程是否正在运行:

$ sudo systemctl status rsyslog

示例输出:

client-rsyslog-service-rhel8

接下来,打开 rsyslog 配置文件:

$ sudo vim /etc/rsyslog.conf

在文件末尾,添加以下行:

*.* @10.128.0.47:514           # Use @ for UDP protocol
*.* @@10.128.0.47:514          # Use @@ for TCP protocol

保存并退出配置文件。就像 Rsyslog 服务器一样,打开 514 端口,这是防火墙上的默认 Rsyslog 端口:

$ sudo firewall-cmd  --add-port=514/tcp  --zone=public  --permanent

接下来,重新加载防火墙以保存更改:

$ sudo firewall-cmd --reload

接下来,重启 rsyslog 服务:

$ sudo systemctl restart rsyslog

要在启动时运行 Rsyslog,请运行以下命令:

$ sudo systemctl enable rsyslog

测试日志记录操作

已经成功安装并配置 Rsyslog 服务器和客户端后,就该验证你的配置是否按预期运行了。

在客户端系统上,运行以下命令:

# logger "Hello guys! This is our first log"

现在进入 Rsyslog 服务器并运行以下命令来实时查看日志消息:

# tail -f /var/log/messages

客户端系统上命令运行的输出显示在了 Rsyslog 服务器的日志中,这意味着 Rsyslog 服务器正在接收来自客户端系统的日志:

centralize-logs-rsyslogs-centos8

就是这些了!我们成功设置了 Rsyslog 服务器来接收来自客户端系统的日志信息。


via: https://www.linuxtechi.com/configure-rsyslog-server-centos-8-rhel-8/

作者:James Kiarie 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你应该已经注意到,在 Linux 中使用 ls 命令 列出的目录内容中,目录的大小仅显示 4KB。这个大小正确吗?如果不正确,那它代表什么,又该如何获取 Linux 中的目录或文件夹大小?这是一个默认的大小,是用来存储磁盘上存储目录的元数据的大小。

Linux 上有一些应用程序可以 获取目录的实际大小。其中,磁盘使用率(du)命令已被 Linux 管理员广泛使用。

我将向您展示如何使用各种选项获取文件夹大小。

什么是 du 命令?

du 命令 表示 磁盘使用率 Disk Usage 。这是一个标准的 Unix 程序,用于估计当前工作目录中的文件空间使用情况。

它使用递归方式总结磁盘使用情况,以获取目录及其子目录的大小。

如同我说的那样, 使用 ls 命令时,目录大小仅显示 4KB。参见下面的输出。

$ ls -lh | grep ^d

drwxr-xr-x  3 daygeek daygeek 4.0K Aug  2 13:57 Bank_Details
drwxr-xr-x  2 daygeek daygeek 4.0K Mar 15  2019 daygeek
drwxr-xr-x  6 daygeek daygeek 4.0K Feb 16  2019 drive-2daygeek
drwxr-xr-x 13 daygeek daygeek 4.0K Jan  6  2019 drive-mageshm
drwxr-xr-x 15 daygeek daygeek 4.0K Sep 29 21:32 Thanu_Photos

1) 在 Linux 上如何只获取父目录的大小

使用以下 du 命令格式获取给定目录的总大小。在该示例中,我们将得到 /home/daygeek/Documents 目录的总大小。

$ du -hs /home/daygeek/Documents
或
$ du -h --max-depth=0 /home/daygeek/Documents/
20G    /home/daygeek/Documents

详细说明:

  • du – 这是一个命令
  • -h – 以易读的格式显示大小 (例如 1K 234M 2G)
  • -s – 仅显示每个参数的总数
  • --max-depth=N – 目录的打印深度

2) 在 Linux 上如何获取每个目录的大小

使用以下 du 命令格式获取每个目录(包括子目录)的总大小。

在该示例中,我们将获得每个 /home/daygeek/Documents 目录及其子目录的总大小。

$ du -h /home/daygeek/Documents/ | sort -rh | head -20

20G    /home/daygeek/Documents/
9.6G    /home/daygeek/Documents/drive-2daygeek
6.3G    /home/daygeek/Documents/Thanu_Photos
5.3G    /home/daygeek/Documents/Thanu_Photos/Camera
5.3G    /home/daygeek/Documents/drive-2daygeek/Thanu-videos
3.2G    /home/daygeek/Documents/drive-mageshm
2.3G    /home/daygeek/Documents/drive-2daygeek/Thanu-Photos
2.2G    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month
916M    /home/daygeek/Documents/drive-mageshm/Tanisha
454M    /home/daygeek/Documents/drive-mageshm/2g-backup
415M    /home/daygeek/Documents/Thanu_Photos/WhatsApp Video
300M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Jan-2017
288M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Oct-2017
226M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Sep-2017
219M    /home/daygeek/Documents/Thanu_Photos/WhatsApp Documents
213M    /home/daygeek/Documents/drive-mageshm/photos
163M    /home/daygeek/Documents/Thanu_Photos/WhatsApp Video/Sent
161M    /home/daygeek/Documents/Thanu_Photos/WhatsApp Images
154M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/June-2017
150M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Nov-2016

3) 在 Linux 上如何获取每个目录的摘要

使用如下 du 命令格式仅获取每个目录的摘要。

$ du -hs /home/daygeek/Documents/* | sort -rh | head -10

9.6G    /home/daygeek/Documents/drive-2daygeek
6.3G    /home/daygeek/Documents/Thanu_Photos
3.2G    /home/daygeek/Documents/drive-mageshm
756K    /home/daygeek/Documents/Bank_Details
272K    /home/daygeek/Documents/user-friendly-zorin-os-15-has-been-released-TouchInterface1.png
172K    /home/daygeek/Documents/user-friendly-zorin-os-15-has-been-released-NightLight.png
164K    /home/daygeek/Documents/ConfigServer Security and Firewall (csf) Cheat Sheet.pdf
132K    /home/daygeek/Documents/user-friendly-zorin-os-15-has-been-released-Todo.png
112K    /home/daygeek/Documents/user-friendly-zorin-os-15-has-been-released-ZorinAutoTheme.png
96K    /home/daygeek/Documents/distro-info.xlsx

4) 在 Linux 上如何获取每个目录的不含子目录的大小

使用如下 du 命令格式来展示每个目录的总大小,不包括子目录。

$ du -hS /home/daygeek/Documents/ | sort -rh | head -20

5.3G    /home/daygeek/Documents/Thanu_Photos/Camera
5.3G    /home/daygeek/Documents/drive-2daygeek/Thanu-videos
2.3G    /home/daygeek/Documents/drive-2daygeek/Thanu-Photos
1.5G    /home/daygeek/Documents/drive-mageshm
831M    /home/daygeek/Documents/drive-mageshm/Tanisha
454M    /home/daygeek/Documents/drive-mageshm/2g-backup
300M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Jan-2017
288M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Oct-2017
253M    /home/daygeek/Documents/Thanu_Photos/WhatsApp Video
226M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Sep-2017
219M    /home/daygeek/Documents/Thanu_Photos/WhatsApp Documents
213M    /home/daygeek/Documents/drive-mageshm/photos
163M    /home/daygeek/Documents/Thanu_Photos/WhatsApp Video/Sent
154M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/June-2017
150M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Nov-2016
127M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Dec-2016
100M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Oct-2016
94M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Nov-2017
92M    /home/daygeek/Documents/Thanu_Photos/WhatsApp Images
90M    /home/daygeek/Documents/drive-2daygeek/Thanu-photos-by-month/Dec-2017

5) 在 Linux 上如何仅获取一级子目录的大小

如果要获取 Linux 上给定目录的一级子目录(包括其子目录)的大小,请使用以下命令格式。

$ du -h --max-depth=1 /home/daygeek/Documents/

3.2G    /home/daygeek/Documents/drive-mageshm
4.0K    /home/daygeek/Documents/daygeek
756K    /home/daygeek/Documents/Bank_Details
9.6G    /home/daygeek/Documents/drive-2daygeek
6.3G    /home/daygeek/Documents/Thanu_Photos
20G    /home/daygeek/Documents/

6) 如何在 du 命令输出中获得总计

如果要在 du 命令输出中获得总计,请使用以下 du 命令格式。

$ du -hsc /home/daygeek/Documents/* | sort -rh | head -10

20G    total
9.6G    /home/daygeek/Documents/drive-2daygeek
6.3G    /home/daygeek/Documents/Thanu_Photos
3.2G    /home/daygeek/Documents/drive-mageshm
756K    /home/daygeek/Documents/Bank_Details
272K    /home/daygeek/Documents/user-friendly-zorin-os-15-has-been-released-TouchInterface1.png
172K    /home/daygeek/Documents/user-friendly-zorin-os-15-has-been-released-NightLight.png
164K    /home/daygeek/Documents/ConfigServer Security and Firewall (csf) Cheat Sheet.pdf
132K    /home/daygeek/Documents/user-friendly-zorin-os-15-has-been-released-Todo.png
112K    /home/daygeek/Documents/user-friendly-zorin-os-15-has-been-released-ZorinAutoTheme.png

via: https://www.2daygeek.com/find-get-size-of-directory-folder-linux-disk-usage-du-command/

作者:Magesh Maruthamuthu 选题:lujun9972 译者:lnrCoder 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

当你想要争论代码复杂性时,Pylint 是你的朋友。

 title= in VIM")

Pylint 是更高层级的 Python 样式强制程序。而 flake8black 检查的是“本地”样式:换行位置、注释的格式、发现注释掉的代码或日志格式中的错误做法之类的问题。

默认情况下,Pylint 非常激进。它将对每样东西都提供严厉的意见,从检查是否实际实现声明的接口到重构重复代码的可能性,这对新用户来说可能会很多。一种温和地将其引入项目或团队的方法是先关闭所有检查器,然后逐个启用检查器。如果你已经在使用 flake8、black 和 mypy,这尤其有用:Pylint 有相当多的检查器和它们在功能上重叠。

但是,Pylint 独有之处之一是能够强制执行更高级别的问题:例如,函数的行数或者类中方法的数量。

这些数字可能因项目而异,并且可能取决于开发团队的偏好。但是,一旦团队就参数达成一致,使用自动工具强制化这些参数非常有用。这是 Pylint 闪耀的地方。

配置 Pylint

要以空配置开始,请将 .pylintrc 设置为

[MESSAGES CONTROL]

disable=all

这将禁用所有 Pylint 消息。由于其中许多是冗余的,这是有道理的。在 Pylint 中,message 是一种特定的警告。

你可以通过运行 pylint 来确认所有消息都已关闭:

$ pylint <my package>

通常,向 pylint 命令行添加参数并不是一个好主意:配置 pylint 的最佳位置是 .pylintrc。为了使它做一些有用的事,我们需要启用一些消息。

要启用消息,在 .pylintrc 中的 [MESSAGES CONTROL] 下添加

enable=<message>,
       ...

对于看起来有用的“消息”(Pylint 称之为不同类型的警告)。我最喜欢的包括 too-many-linestoo-many-argumentstoo-many-branches。所有这些会限制模块或函数的复杂性,并且无需进行人工操作即可客观地进行代码复杂度测量。

检查器消息的来源:每条消息只属于一个检查器。许多最有用的消息都在设计检查器下。默认数字通常都不错,但要调整最大值也很简单:我们可以在 .pylintrc 中添加一个名为 DESIGN 的段。

[DESIGN]
max-args=7
max-locals=15

另一个有用的消息来源是“重构”检查器。我已启用一些最喜欢的消息有 consider-using-dict-comprehensionstop-iteration-return(它会查找正确的停止迭代的方式是 return 而使用了 raise StopIteration 的迭代器)和 chained-comparison,它将建议使用如 1 <= x < 5,而不是不太明显的 1 <= x && 5 > 5 的语法。

最后是一个在性能方面消耗很大的检查器,但它非常有用,就是 similarities。它会查找不同部分代码之间的复制粘贴来强制执行“不要重复自己”(DRY 原则)。它只启用一条消息:duplicate-code。默认的 “最小相似行数” 设置为 4。可以使用 .pylintrc 将其设置为不同的值。

[SIMILARITIES]
min-similarity-lines=3

Pylint 使代码评审变得简单

如果你厌倦了需要指出一个类太复杂,或者两个不同的函数基本相同的代码评审,请将 Pylint 添加到你的持续集成配置中,并且只需要对项目复杂性准则的争论一次就行。


via: https://opensource.com/article/19/10/python-pylint-introduction

作者:Moshe Zadka 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出